Ich versuche zu verstehen, wie CPython GIL funktioniert und was die Unterschiede zwischen GIL in CPython 2.7.x und CPython 3.4.x sind. Ich verwende diesen Code für das Benchmarking:Warum läuft dieses Python-Skript auf mehreren Kernen 4x langsamer als auf einem einzigen Kern?
from __future__ import print_function
import argparse
import resource
import sys
import threading
import time
def countdown(n):
while n > 0:
n -= 1
def get_time():
stats = resource.getrusage(resource.RUSAGE_SELF)
total_cpu_time = stats.ru_utime + stats.ru_stime
return time.time(), total_cpu_time, stats.ru_utime, stats.ru_stime
def get_time_diff(start_time, end_time):
return tuple((end-start) for start, end in zip(start_time, end_time))
def main(total_cycles, max_threads, no_headers=False):
header = ("%4s %8s %8s %8s %8s %8s %8s %8s %8s" %
("#t", "seq_r", "seq_c", "seq_u", "seq_s",
"par_r", "par_c", "par_u", "par_s"))
row_format = ("%(threads)4d "
"%(seq_r)8.2f %(seq_c)8.2f %(seq_u)8.2f %(seq_s)8.2f "
"%(par_r)8.2f %(par_c)8.2f %(par_u)8.2f %(par_s)8.2f")
if not no_headers:
print(header)
for thread_count in range(1, max_threads+1):
# We don't care about a few lost cycles
cycles = total_cycles // thread_count
threads = [threading.Thread(target=countdown, args=(cycles,))
for i in range(thread_count)]
start_time = get_time()
for thread in threads:
thread.start()
thread.join()
end_time = get_time()
sequential = get_time_diff(start_time, end_time)
threads = [threading.Thread(target=countdown, args=(cycles,))
for i in range(thread_count)]
start_time = get_time()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
end_time = get_time()
parallel = get_time_diff(start_time, end_time)
print(row_format % {"threads": thread_count,
"seq_r": sequential[0],
"seq_c": sequential[1],
"seq_u": sequential[2],
"seq_s": sequential[3],
"par_r": parallel[0],
"par_c": parallel[1],
"par_u": parallel[2],
"par_s": parallel[3]})
if __name__ == "__main__":
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("max_threads", nargs="?",
type=int, default=5)
arg_parser.add_argument("total_cycles", nargs="?",
type=int, default=50000000)
arg_parser.add_argument("--no-headers",
action="store_true")
args = arg_parser.parse_args()
sys.exit(main(args.total_cycles, args.max_threads, args.no_headers))
Wenn dieses Skript läuft auf meiner Quad-Core-i5-2500 Maschine unter Ubuntu 14.04 mit Python 2.7.6, erhalte ich die folgenden Ergebnisse (_R für Echtzeit steht, _c für CPU-Zeit, _u für Benutzermodus, _S für Kernel-Modus):
#t seq_r seq_c seq_u seq_s par_r par_c par_u par_s
1 1.47 1.47 1.47 0.00 1.46 1.46 1.46 0.00
2 1.74 1.74 1.74 0.00 3.33 5.45 3.52 1.93
3 1.87 1.90 1.90 0.00 3.08 6.42 3.77 2.65
4 1.78 1.83 1.83 0.00 3.73 6.18 3.88 2.30
5 1.73 1.79 1.79 0.00 3.74 6.26 3.87 2.39
Nun, wenn ich alle Themen auf einen Kern zu binden, sind die Ergebnisse sehr unterschiedlich:
taskset -c 0 python countdown.py
#t seq_r seq_c seq_u seq_s par_r par_c par_u par_s
1 1.46 1.46 1.46 0.00 1.46 1.46 1.46 0.00
2 1.74 1.74 1.73 0.00 1.69 1.68 1.68 0.00
3 1.47 1.47 1.47 0.00 1.58 1.58 1.54 0.04
4 1.74 1.74 1.74 0.00 2.02 2.02 1.87 0.15
5 1.46 1.46 1.46 0.00 1.91 1.90 1.75 0.15
So ist die Frage : Warum läuft dieser Python? Code auf mehreren Kernen ist 1.5x-2x langsamer durch die Wanduhr und 4x-5x langsamer durch CPU-Takt als auf einem einzelnen Kern laufen?
herum Vorstellung und zwei Hypothesen erzeugt googeln:
- Wenn auf mehreren Kernen ausgeführt wird, kann ein Thread neu geplant werden, auf einem anderen Kern laufen, was bedeutet, dass die lokalen Cache, die Verlangsamung somit für ungültig erklärt wird.
- Der Overhead zum Aufhängen eines Threads auf einem Kern und Aktivieren auf einem anderen Kern ist größer als das Aufheben und Aktivieren des Threads auf demselben Kern.
Gibt es noch andere Gründe? Ich würde gerne verstehen, was vor sich geht, und mein Verständnis mit Zahlen untermauern können (das heißt, wenn die Verlangsamung auf Cache-Misses zurückzuführen ist, möchte ich die Zahlen für beide Fälle sehen und vergleichen).
Ja, ich kenne die GIL und es überrascht mich nicht, dass das Ausführen von Countdown in parallelen Threads tatsächlich langsamer als in einem einzelnen Thread ist. Was mich überrascht und was ich nicht verstehe, ist, warum das Ausführen dieses Skripts auf mehreren Kernen so viel langsamer ist als das Ausführen auf einem einzigen Kern. – user108884
Mir ist aufgefallen, dass beim Addieren der in der ersten Version gemeldeten Zeiten (also ohne Taskset) die Summe nicht mit der von 'time' angegebenen Zeit übereinstimmt. Wenn 'time.clock()' in 'time.time()' geändert wird, verschwindet diese Diskrepanz. Es scheint immer noch einen kleinen Vorteil zu geben, wenn man den "taskset" -Ansatz verwendet, nicht sicher, was das alles bedeutet ... – brm
Ein * nix time.clock() meldet CPU-Zeit, nicht Wanduhrzeit (https: // docs. python.org/2.7/library/time.html#time.clock). Daher sollten die Ergebnisse so interpretiert werden: Es erfordert viel mehr CPU-Aufwand, um diesen Code auf mehreren Kernen als auf einem einzelnen Kern auszuführen. Ich bin nicht der erste, der über diese Ergebnisse stolpert (z. B. https://youtu.be/Obt-vMVdM8s?t=55s), aber ich bin nicht mit der Erklärung zufrieden. Aber du hast Recht, ich sollte auch in Echtzeit messen und berichten. Ich werde den Code aktualisieren. – user108884