Próbuję zrozumieć, jak działa GIL programu CPython i jakie są różnice między GIL w CPython 2.7.x i CPython 3.4.x. Używam tego kodu do benchmarkingu:Dlaczego ten skrypt Pythona działa 4x wolniej na wielu rdzeniach niż na jednym rdzeniu
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))
Po uruchomieniu tego skryptu na mój quad-core i5-2500 maszynie pod Ubuntu 14.04 z Python 2.7.6, mam następujące wyniki (_R podpórek czasie rzeczywistym, _C do czasu procesora, _U dla trybu użytkownika, _S dla trybu jądra):
#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
teraz jeśli wiążę wszystkie wątki do jednego rdzenia, wyniki są bardzo różne:
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
Więc pytanie jest : po co uruchamiać ten Python kod na wielu rdzeniach jest 1,5x-2x wolniejszy przez zegar ścienny i 4 x 5 razy wolniejszy w porównaniu z zegarem procesora, niż na pojedynczym rdzeniu?
wyjściowa wokół i googling produkowane dwie hipotezy:
- Podczas pracy na wielu rdzeniach, nitki mogą być ponownie zaplanowane na innym rdzeniu co oznacza, że lokalna pamięć podręczna zostanie unieważniony, stąd spowolnienie.
- Koszt zawieszania nici na jednym rdzeniu i aktywowania go na innym rdzeniu jest większy niż zawieszanie i aktywowanie wątku na tym samym rdzeniu.
Czy są jakieś inne powody? Chciałbym zrozumieć, co się dzieje i być w stanie poprzeć moje zrozumienie liczbami (co oznacza, że jeśli spowolnienie jest spowodowane brakami w pamięci podręcznej, chcę zobaczyć i porównać liczby dla obu przypadków).
Tak, jestem świadomy GIL i nie dziwi mnie, że odliczanie w równoległych wątkach jest wolniejsze niż w pojedynczym wątku. Co mnie zaskakuje i czego nie rozumiem, to dlaczego uruchamianie tego skryptu na wielu rdzeniach jest znacznie wolniejsze niż uruchamianie go na jednym rdzeniu. – user108884
Zauważyłem, że dodając czasy podane w pierwszej wersji (czyli bez zestawu zadań), suma nie odpowiadała czasowi podanemu przez 'time'. Jeśli 'time.clock()' zostanie zmieniony na 'time.time()', rozbieżność zniknie. Nadal wydaje się być niewielka przewaga przy korzystaniu z 'zestawu zadań' jednak nie wiem co to wszystko znaczy ... – brm
Włącz * nix time.clock() zgłasza czas procesora, a nie czas zegara ściennego (https: // docs. python.org/2.7/library/time.html#time.clock). Tak więc wyniki powinny być interpretowane w ten sposób: potrzeba dużo więcej wysiłku, aby uruchomić ten kod na wielu rdzeniach niż na pojedynczym rdzeniu. Nie jestem pierwszym, który potknął się o te wyniki (np. Https://youtu.be/Obt-vMVdM8s?t=55s), ale nie jestem usatysfakcjonowany wyjaśnieniem. Ale masz rację, powinienem mierzyć i raportować również w czasie rzeczywistym. Zaktualizuję kod. – user108884