Suivi Python GIL





Il existe de nombreux articles expliquant à quoi sert le Python GIL (The Global Interpreter Lock) (je veux dire CPython). En bref, le GIL empêche le code Python propre multithread d'utiliser plusieurs cœurs de processeur.



Cependant, chez Vaex, nous effectuons la plupart des tâches intensives en calcul en C ++ avec le GIL désactivé. C'est une pratique normale pour les bibliothèques Python hautes performances, où Python n'agit que comme une colle de haut niveau.



Le GIL doit être désactivé explicitement, et c'est la responsabilité du programmeur, qu'il peut oublier, ce qui conduira à une utilisation inefficace de la capacité. Récemment, j'ai moi-même été dans le rôle de l'oubli et j'ai trouvé un problème similaire dans Apache Arrow(il s'agit d'une dépendance Vaex, donc lorsque le GIL n'est pas désactivé dans Arrow, nous (et tout le monde) subissons un impact sur les performances).



De plus, lors de l'exécution sur 64 cœurs, les performances Vaex sont parfois loin d'être idéales. Il peut utiliser 4000% du processeur au lieu de 6400%, ce qui ne me convient pas. Au lieu d'insérer au hasard des commutateurs pour étudier cet effet, je veux comprendre ce qui se passe, et si le problème est dans le GIL, alors je veux comprendre pourquoi et comment cela ralentit Vaex.



Pourquoi est-ce que j'écris ceci



Je prévois d'écrire une série d'articles couvrant certains des outils et techniques de profilage et de traçage de Python avec des extensions natives, et comment ces outils peuvent être combinés pour analyser et visualiser les performances de Python avec le GIL activé et désactivé.



Espérons que cela aidera à améliorer le traçage, le profilage et d'autres mesures de performances dans l'écosystème du langage, ainsi que les performances de l'ensemble de l'écosystème Python.



Exigences



Linux



Vous avez besoin d'un accès root à la machine Linux (sudo suffit). Ou demandez à votre administrateur système d'exécuter les commandes ci-dessous pour vous. Pour le reste de cet article, les privilèges utilisateur sont suffisants.



Perf



Assurez-vous que perf est installé, par exemple, sur Ubuntu, vous pouvez le faire comme ceci:



$ sudo yum install perf

      
      





Configuration du noyau



Exécuter en tant qu'utilisateur:



# Enable users to run perf (use at own risk)
$ sudo sysctl kernel.perf_event_paranoid=-1

# Enable users to see schedule trace events:
$ sudo mount -o remount,mode=755 /sys/kernel/debug
$ sudo mount -o remount,mode=755 /sys/kernel/debug/tracing

      
      





Paquets Python



Nous utiliserons VizTracer et per4m



$ pip install "viztracer>=0.11.2" "per4m>=0.1,<0.2"

      
      





Suivi de l'état des threads et des processus avec perf



Il n'y a aucun moyen de comprendre l'état du GIL en Python (autre que d'utiliser l' interrogation ) car il n'y a pas d'API pour cela. Nous pouvons surveiller l'état du noyau, et pour cela nous avons besoin de l'outil perf .



Avec son aide (également appelée perf_events), nous pouvons écouter les changements dans l'état des processus et des threads (nous ne sommes intéressés que par le sommeil et l'exécution) et les consigner. Perf peut sembler intimidant, mais c'est un outil puissant. Si vous voulez en savoir plus, je vous recommande de lire l' article de Julia Evans ou le site de Brendan Gregg .



Pour vous connecter, appliquons d'abord perf à un programme simple :



import time
from threading import Thread

def sleep_a_bit():
    time.sleep(1)

def main():
    t = Thread(target=sleep_a_bit)
    t.start()
    t.join()

main()

      
      





Nous n'écoutons que quelques événements pour réduire le bruit (notez l'utilisation de jokers):



$ perf record -e sched:sched_switch  -e sched:sched_process_fork \
        -e 'sched:sched_wak*' -- python -m per4m.example0
[ perf record: Woken up 2 times to write data ]
[ perf record: Captured and wrote 0,032 MB perf.data (33 samples) ]

      
      





Et nous utiliserons la commande perf script pour générer un résultat lisible adapté à l'analyse.



Texte masqué
$ perf script
        :3040108 3040108 [032] 5563910.979408:                sched:sched_waking: comm=perf pid=3040114 prio=120 target_cpu=031
        :3040108 3040108 [032] 5563910.979431:                sched:sched_wakeup: comm=perf pid=3040114 prio=120 target_cpu=031
          python 3040114 [031] 5563910.995616:                sched:sched_waking: comm=kworker/31:1 pid=2502104 prio=120 target_cpu=031
          python 3040114 [031] 5563910.995618:                sched:sched_wakeup: comm=kworker/31:1 pid=2502104 prio=120 target_cpu=031
          python 3040114 [031] 5563910.995621:                sched:sched_waking: comm=ksoftirqd/31 pid=198 prio=120 target_cpu=031
          python 3040114 [031] 5563910.995622:                sched:sched_wakeup: comm=ksoftirqd/31 pid=198 prio=120 target_cpu=031
          python 3040114 [031] 5563910.995624:                sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=R+ ==> next_comm=kworker/31:1 next_pid=2502104 next_prio=120
          python 3040114 [031] 5563911.003612:                sched:sched_waking: comm=kworker/32:1 pid=2467833 prio=120 target_cpu=032
          python 3040114 [031] 5563911.003614:                sched:sched_wakeup: comm=kworker/32:1 pid=2467833 prio=120 target_cpu=032
          python 3040114 [031] 5563911.083609:                sched:sched_waking: comm=ksoftirqd/31 pid=198 prio=120 target_cpu=031
          python 3040114 [031] 5563911.083612:                sched:sched_wakeup: comm=ksoftirqd/31 pid=198 prio=120 target_cpu=031
          python 3040114 [031] 5563911.083613:                sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=R ==> next_comm=ksoftirqd/31 next_pid=198 next_prio=120
          python 3040114 [031] 5563911.108984:                sched:sched_waking: comm=node pid=2446812 prio=120 target_cpu=045
          python 3040114 [031] 5563911.109059:                sched:sched_waking: comm=node pid=2446812 prio=120 target_cpu=045
          python 3040114 [031] 5563911.112250:          sched:sched_process_fork: comm=python pid=3040114 child_comm=python child_pid=3040116
          python 3040114 [031] 5563911.112260:            sched:sched_wakeup_new: comm=python pid=3040116 prio=120 target_cpu=037
          python 3040114 [031] 5563911.112262:            sched:sched_wakeup_new: comm=python pid=3040116 prio=120 target_cpu=037
          python 3040114 [031] 5563911.112273:                sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=S ==> next_comm=swapper/31 next_pid=0 next_prio=120
          python 3040116 [037] 5563911.112418:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040116 [037] 5563911.112450:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040116 [037] 5563911.112473: sched:sched_wake_idle_without_ipi: cpu=31
         swapper     0 [031] 5563911.112476:                sched:sched_wakeup: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040114 [031] 5563911.112485:                sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=S ==> next_comm=swapper/31 next_pid=0 next_prio=120
          python 3040116 [037] 5563911.112485:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040116 [037] 5563911.112489:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040116 [037] 5563911.112496:                sched:sched_switch: prev_comm=python prev_pid=3040116 prev_prio=120 prev_state=S ==> next_comm=swapper/37 next_pid=0 next_prio=120
         swapper     0 [031] 5563911.112497:                sched:sched_wakeup: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040114 [031] 5563911.112513:                sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=S ==> next_comm=swapper/31 next_pid=0 next_prio=120
         swapper     0 [037] 5563912.113490:                sched:sched_waking: comm=python pid=3040116 prio=120 target_cpu=037
         swapper     0 [037] 5563912.113529:                sched:sched_wakeup: comm=python pid=3040116 prio=120 target_cpu=037
          python 3040116 [037] 5563912.113595:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
          python 3040116 [037] 5563912.113620:                sched:sched_waking: comm=python pid=3040114 prio=120 target_cpu=031
         swapper     0 [031] 5563912.113697:                sched:sched_wakeup: comm=python pid=3040114 prio=120 target_cpu=031

      
      







Jetez un œil à la quatrième colonne (temps en secondes): le programme s'est endormi (une seconde s'est écoulée). Ici, nous voyons l'entrée de l'état de sommeil:



python 3040114 [031] 5563911.112513: sched:sched_switch: prev_comm=python prev_pid=3040114 prev_prio=120 prev_state=S ==> next_comm=swapper/31 next_pid=0 next_prio=120

      
      





Cela signifie que le noyau a changé l'état du thread Python en S



(= dormant).



Une seconde plus tard, le programme s'est réveillé:



swapper 0 [031] 5563912.113697: sched:sched_wakeup: comm=python pid=3040114 prio=120 target_cpu=031

      
      





Bien sûr, pour comprendre ce qui se passe, vous devrez collecter les outils. Oui, le résultat peut également être facilement analysé à l'aide de per4m , mais avant de continuer, je souhaite visualiser le flux d'un programme légèrement plus complexe à l' aide de VizTracer .



VizTracer



C'est un traceur Python capable de visualiser le travail qu'un programme est en train de faire dans un navigateur. Appliquons-le à un programme plus complexe :



import threading
import time

def some_computation():
    total = 0   
    for i in range(1_000_000):
        total += i
    return total

def main():
    thread1 = threading.Thread(target=some_computation)
    thread2 = threading.Thread(target=some_computation)
    thread1.start()
    thread2.start()
    time.sleep(0.2)
    for thread in [thread1, thread2]:
        thread.join()

main()

      
      





Le résultat de l'opération de traceur:



$ viztracer -o example1.html --ignore_frozen -m per4m.example1
Loading finish                                        
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/example1.html ...
Dumping trace data to json, total entries: 94, estimated json file size: 11.0KiB
Generating HTML report
Report saved.

      
      





Voici à quoi ressemble le HTML résultant:





Il semble avoir été some_computation



exécuté en parallèle (deux fois), bien que nous sachions que le GIL l'empêche. Que se passe t-il ici?



Combinaison de VizTracer et de résultats de perf



Appliquons perf comme dans example0.py. Seulement maintenant, ajoutons un argument -k CLOCK_MONOTONIC



pour utiliser la même horloge que VizTracer et lui demandons de générer du JSON au lieu de HTML:



$ perf record -e sched:sched_switch  -e sched:sched_process_fork -e 'sched:sched_wak*' \
   -k CLOCK_MONOTONIC  -- viztracer -o viztracer1.json --ignore_frozen -m per4m.example1

      
      





Nous utilisons ensuite per4m pour convertir les résultats du script perf en JSON que VizTracer peut lire:



$ perf script | per4m perf2trace sched -o perf1.json
Wrote to perf1.json

      
      





Maintenant, en utilisant VizTracer, combinons les deux fichiers JSON:



$ viztracer --combine perf1.json viztracer1.json -o example1_state.html
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/example1.html ...
Dumping trace data to json, total entries: 131, estimated json file size: 15.4KiB
Generating HTML report
Report saved.

      
      





Voici ce que nous avons:





De toute évidence, les threads dorment régulièrement à cause du GIL et ne fonctionnent pas en parallèle.



Remarque : la phase de veille dure environ 5 ms, ce qui correspond à l' intervalle par défaut sys.getswitchinterval



Définition GIL



Notre processus s'endort, mais nous ne voyons pas la différence entre les états de sommeil qui sont initiés par un appel time.sleep



ou le GIL. Il y a plusieurs façons de faire la différence, regardons-en deux.



À travers la trace de la pile



Avec de l'aide perf record -g



(ou mieux avec perf record --call-graph dwarf



, cela implique -g



), nous obtenons les traces de pile pour chaque événement perf.



$ perf record -e sched:sched_switch  -e sched:sched_process_fork -e 'sched:sched_wak*'\
   -k CLOCK_MONOTONIC  --call-graph dwarf -- viztracer -o viztracer1-gil.json --ignore_frozen -m per4m.example1
Loading finish                                        
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/viztracer1-gil.json ...
Dumping trace data to json, total entries: 94, estimated json file size: 11.0KiB
Report saved.
[ perf record: Woken up 3 times to write data ]
[ perf record: Captured and wrote 0,991 MB perf.data (164 samples) ]

      
      





En regardant le résultat du script perf (que nous avons ajouté pour des raisons de performances --no-inline



), nous obtenons beaucoup d'informations. Regardez l'événement de changement d'état, il s'avère que take_gil a été appelé !



Texte masqué
$ perf script --no-inline | less
...
viztracer 3306851 [059] 5614683.022539:                sched:sched_switch: prev_comm=viztracer prev_pid=3306851 prev_prio=120 prev_state=S ==> next_comm=swapper/59 next_pid=0 next_prio=120
        ffffffff96ed4785 __sched_text_start+0x375 ([kernel.kallsyms])
        ffffffff96ed4785 __sched_text_start+0x375 ([kernel.kallsyms])
        ffffffff96ed4b92 schedule+0x42 ([kernel.kallsyms])
        ffffffff9654a51b futex_wait_queue_me+0xbb ([kernel.kallsyms])
        ffffffff9654ac85 futex_wait+0x105 ([kernel.kallsyms])
        ffffffff9654daff do_futex+0x10f ([kernel.kallsyms])
        ffffffff9654dfef __x64_sys_futex+0x13f ([kernel.kallsyms])
        ffffffff964044c7 do_syscall_64+0x57 ([kernel.kallsyms])
        ffffffff9700008c entry_SYSCALL_64_after_hwframe+0x44 ([kernel.kallsyms])
        7f4884b977b1<a href="https://www.maartenbreddels.com/cdn-cgi/l/email-protection"> [email protected]@GLIBC_2.3.2+0x271 (/usr/lib/x86_64-linux-gnu/libpthread-2.31.so)
            55595c07fe6d take_gil+0x1ad (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfaa0b3 PyEval_RestoreThread+0x23 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c000872 lock_PyThread_acquire_lock+0x1d2 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfe71f3 _PyMethodDef_RawFastCallKeywords+0x263 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfe7313 _PyCFunction_FastCallKeywords+0x23 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d657 call_function+0x3b7 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb6db1 _PyEval_EvalCodeWithName+0x251 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6b00 _PyFunction_FastCallKeywords+0x520 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb6db1 _PyEval_EvalCodeWithName+0x251 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6b00 _PyFunction_FastCallKeywords+0x520 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6766 _PyFunction_FastCallKeywords+0x186 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6766 _PyFunction_FastCallKeywords+0x186 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060ae4 _PyEval_EvalFrameDefault+0x3f4 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb6db1 _PyEval_EvalCodeWithName+0x251 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c074e5d builtin_exec+0x33d (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfe7078 _PyMethodDef_RawFastCallKeywords+0xe8 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfe7313 _PyCFunction_FastCallKeywords+0x23 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c066c39 _PyEval_EvalFrameDefault+0x6549 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb77e0 _PyEval_EvalCodeWithName+0xc80 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6b62 _PyFunction_FastCallKeywords+0x582 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6766 _PyFunction_FastCallKeywords+0x186 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6766 _PyFunction_FastCallKeywords+0x186 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c01d334 call_function+0x94 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060d00 _PyEval_EvalFrameDefault+0x610 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfd6766 _PyFunction_FastCallKeywords+0x186 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c060ae4 _PyEval_EvalFrameDefault+0x3f4 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb6db1 _PyEval_EvalCodeWithName+0x251 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595bfb81e2 PyEval_EvalCode+0x22 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c0c51d1 run_mod+0x31 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c0cf31d PyRun_FileExFlags+0x9d (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c0cf50a PyRun_SimpleFileExFlags+0x1ba (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c0d05f0 pymain_main+0x3e0 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            55595c0d067b _Py_UnixMain+0x3b (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
            7f48849bc0b2 __libc_start_main+0xf2 (/usr/lib/x86_64-linux-gnu/libc-2.31.so)
            55595c075100 _start+0x28 (/home/maartenbreddels/miniconda/envs/dev/bin/python3.7)

      
      







Remarque : également appelé pthread_cond_timedwait



, il est utilisé par https://github.com/sumerc/gilstats.py pour eBPF si vous êtes intéressé par d'autres mutex.



Une autre note : notez qu'il n'y a pas de trace de pile Python ici, mais à la place nous en avons _PyEval_EvalFrameDefault



plus. À l'avenir, je prévois d'écrire comment insérer une trace de pile.



L'outil de conversion per4m perf2trace



comprend cela et génère un résultat différent lorsque la trace contient take_gil



:



$ perf script --no-inline | per4m perf2trace sched -o perf1-gil.json
Wrote to perf1-gil.json
$ viztracer --combine perf1-gil.json viztracer1-gil.json -o example1-gil.html
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/example1.html ...
Dumping trace data to json, total entries: 131, estimated json file size: 15.4KiB
Generating HTML report
Report saved.

      
      





On a:





Maintenant, nous pouvons voir exactement où le GIL entre en jeu!



Par sondage (kprobes / uprobes)



Nous savons quand les processus se mettent en veille (en raison du GIL ou pour d'autres raisons), mais si vous voulez en savoir plus sur le moment où le GIL est activé ou désactivé, vous devez savoir quand les résultats sont appelés et renvoyés take_gil



et drop_gil



. Cette trace peut être obtenue en sondant avec perf. Dans un environnement utilisateur, les sondes sont des uprobes, qui sont analogues aux kprobes, qui, comme vous l'avez peut-être deviné, fonctionnent dans l'environnement du noyau. Encore une fois, Julia Evans est une excellente source d'informations supplémentaires .



Installez 4 sondes:



sudo perf probe -f -x `which python` python:take_gil=take_gil
sudo perf probe -f -x `which python` python:take_gil=take_gil%return
sudo perf probe -f -x `which python` python:drop_gil=drop_gil
sudo perf probe -f -x `which python` python:drop_gil=drop_gil%return

Added new events:

  python:take_gil      (on take_gil in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
  python:take_gil_1    (on take_gil in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)

You can now use it in all perf tools, such as:

        perf record -e python:take_gil_1 -aR sleep 1

Failed to find "take_gil%return",
 because take_gil is an inlined function and has no return point.
Added new event:
  python:take_gil__return (on take_gil%return in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)

You can now use it in all perf tools, such as:

        perf record -e python:take_gil__return -aR sleep 1

Added new events:
  python:drop_gil      (on drop_gil in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)
  python:drop_gil_1    (on drop_gil in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)

You can now use it in all perf tools, such as:

        perf record -e python:drop_gil_1 -aR sleep 1

Failed to find "drop_gil%return",
 because drop_gil is an inlined function and has no return point.
Added new event:
  python:drop_gil__return (on drop_gil%return in /home/maartenbreddels/miniconda/envs/dev/bin/python3.7)

You can now use it in all perf tools, such as:

        perf record -e python:drop_gil__return -aR sleep 1

      
      





Il y a quelques plaintes, et à cause de celles en ligne drop_gil



, take_gil



plusieurs sondes / événements ont été ajoutés (c'est-à-dire que la fonction est présentée plusieurs fois dans le fichier binaire), mais tout fonctionne.



Remarque : j'ai peut-être eu de la chance que mon binaire Python (de conda-forge) ait été compilé afin que le take_gil



/ correspondant drop_gil



(et leurs résultats) fonctionne avec succès pour résoudre ce problème.



Veuillez noter que les sondes n'affectent pas les performances, et seulement lorsqu'elles sont "actives" (par exemple, lorsque nous les surveillons à partir de perf), elles exécutent le code sur une branche différente. Pendant la surveillance, les pages affectées sont copiées pour le processus surveillé et les points de contrôle sont insérés aux bons endroits(INT3 pour les processeurs x86). Le point de contrôle déclenche un événement pour perf avec peu de frais généraux. Si vous souhaitez supprimer les sondes, exécutez la commande:



$ sudo perf probe --del 'python*'

      
      





Maintenant perf connaît les nouveaux événements qu'il peut écouter, alors réexécutons-le avec un argument supplémentaire -e 'python:*gil*'



:



$ perf record -e sched:sched_switch  -e sched:sched_process_fork -e 'sched:sched_wak*' -k CLOCK_MONOTONIC  \
  -e 'python:*gil*' -- viztracer  -o viztracer1-uprobes.json --ignore_frozen -m per4m.example1

      
      





Remarque : nous l'avons supprimé --call-graph dwarf



, sinon perf ne sera pas à temps et nous perdrons des événements.



Ensuite, en utilisant per4m perf2trace, nous le convertissons en JSON, ce qui est compréhensible pour VizTracer, et en même temps nous obtenons de nouvelles statistiques.



$ perf script --no-inline | per4m perf2trace gil -o perf1-uprobes.json
...
Summary of threads:

PID         total(us)    no gil%    has gil%    gil wait%
--------  -----------  -----------  ------------  -------------
3353567*     164490.0         65.9          27.3            6.7
3353569       66560.0          0.3          48.2           51.5
3353570       60900.0          0.0          56.4           43.6

High 'no gil' is good, we like low 'has gil',
 and we don't want 'gil wait'. (* indicates main thread)
... 
Wrote to perf1-uprobes.json

      
      





La sous-commande per4m perf2trace gil



donne également comme résultat gil_load . De là, nous apprenons que les deux threads attendent le GIL environ la moitié du temps, comme prévu.



Avec le même fichier perf.data enregistré par perf, nous pouvons également générer des informations sur l'état d'un thread ou d'un processus. Mais comme nous fonctionnions sans traces de pile, nous ne savons pas si le processus a dormi à cause du GIL ou non.



$ perf script --no-inline | per4m perf2trace sched -o perf1-state.json
Wrote to perf1-state.json

      
      





Enfin, mettons les trois résultats ensemble:



$ viztracer --combine perf1-state.json perf1-uprobes.json viztracer1-uprobes.json -o example1-uprobes.html 
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/example1-uprobes.html ...
Dumping trace data to json, total entries: 10484, estimated json file size: 1.2MiB
Generating HTML report
Report saved.

      
      





VizTracer donne une bonne idée de qui avait le GIL et qui l'attendait:





Au-dessus de chaque thread est écrit lorsque le thread ou le processus attend le GIL et lorsqu'il est activé (marqué comme LOCK). Notez que ces périodes se chevauchent avec des périodes où le thread ou le processus est éveillé (en cours d'exécution!). Notez également que nous ne voyons qu'un seul thread ou processus en cours d'exécution, comme il se doit à cause du GIL.



Le temps entre les appels take_gil



, c'est-à-dire entre les verrous (et donc entre dormir ou se réveiller), est exactement le même que dans le tableau ci-dessus dans la colonne gil wait%. La durée d'activation de GIL pour chaque thread, étiquetée LOCK, correspond à l'heure indiquée dans la colonne gil%.



Libérer le Kraken ... ghm, GIL



Nous avons vu comment, lorsqu'un programme Python pur est multithread, le GIL limite les performances en n'autorisant qu'un seul thread ou processus à s'exécuter à la fois (pour un processus Python, bien sûr, et éventuellement dans le futur pour un (sous) interpréteur) . Voyons maintenant ce qui se passe si vous désactivez le GIL, comme cela se produit lors de l'exécution des fonctions NumPy.



Le deuxième exemple s'exécute some_numpy_computation



qui appelle la fonction NumPy M = 4 fois en parallèle sur deux threads, tandis que le thread principal exécute du code Python pur.



import threading
import time
import numpy as np

N = 1024*1024*32
M = 4
x = np.arange(N, dtype='f8')

def some_numpy_computation():
    total = 0
    for i in range(M):
        total += x.sum()
    return total

def main(args=None):
    thread1 = threading.Thread(target=some_numpy_computation)
    thread2 = threading.Thread(target=some_numpy_computation)
    thread1.start()
    thread2.start()
    total = 0
    for i in range(2_000_000):
        total += i
    for thread in [thread1, thread2]:
        thread.join()
main()

      
      





Au lieu d'exécuter ce script avec perf et VizTracer, nous utiliserons un utilitaire per4m giltracer



qui automatise toutes les étapes ci-dessus. Elle le rendra un peu plus intelligent. Fondamentalement, nous allons exécuter perf deux fois, une fois sans trace de pile, et la deuxième fois avec, et importer le module / script avant d'exécuter sa fonction principale pour se débarrasser d'une trace inintéressante comme la même importation. Cela se produira assez rapidement pour que nous ne perdions pas d'événements.



$ giltracer --state-detect -o example2-uprobes.html -m per4m.example2
...

      
      





Totaux des flux:



PID         total(us)    no gil%    has gil%    gil wait%
--------  -----------  -----------  ------------  -------------
3373601*    1359990.0         95.8           4.2            0.1
3373683       60276.4         84.6           2.2           13.2
3373684       57324.0         89.2           1.9            8.9

High 'no gil' is good, we like low 'has gil',
 and we don't want 'gil wait'. (* indicates main thread)
...
Saving report to /home/maartenbreddels/github/maartenbreddels/per4m/example2-uprobes.html ...
...

      
      







Bien que le thread principal exécute du code Python (GIL est activé pour lui, ce qui est désigné par le mot LOCK), d'autres threads s'exécutent également en parallèle. Notez que dans l'exemple Python pur, nous avons un thread ou un processus en cours d'exécution en même temps. Et ici en fait trois threads sont exécutés en parallèle. Cela est possible car les routines NumPy incluses avec C / C ++ / Fortran ont désactivé le GIL.



Cependant, le GIL affecte toujours les threads, car lorsque la fonction NumPy revient à Python, elle a besoin de récupérer le GIL, comme on le voit dans les longs blocs take_gil



. Cela prend 10% du temps de chaque thread.



Intégration Jupyter



Étant donné que mon flux de travail implique souvent d'exécuter un MacBook (qui n'exécute pas perf, mais prend en charge dtrace) connecté à une machine Linux à distance, j'utilise le notebook Jupyter pour exécuter du code à distance. Et comme je suis un développeur Jupyter, j'ai dû faire un wrapper avec cell magic



.



# this registers the giltracer cell magic
%load_ext per4m

%%giltracer
# this call the main define above, but this can also be a multiline code cell
main()

Saving report to /tmp/tmpvi8zw9ut/viztracer.json ...
Dumping trace data to json, total entries: 117, estimated json file size: 13.7KiB
Report saved.

[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0,094 MB /tmp/tmpvi8zw9ut/perf.data (244 samples) ]

Wait for perf to finish...
perf script -i /tmp/tmpvi8zw9ut/perf.data --no-inline --ns | per4m perf2trace gil -o /tmp/tmpvi8zw9ut/giltracer.json -q -v 
Saving report to /home/maartenbreddels/github/maartenbreddels/fastblog/_notebooks/giltracer.html ...
Dumping trace data to json, total entries: 849, estimated json file size: 99.5KiB
Generating HTML report
Report saved.

      
      





Télécharger giltracer.html



Ouvrez giltracer.html dans un nouvel onglet (peut ne pas fonctionner pour des raisons de sécurité)



Conclusion



Avec perf, nous pouvons déterminer les états des processus ou des threads, ce qui aide à comprendre lequel d'entre eux a le GIL activé en Python. Et à l'aide des traces de pile, vous pouvez déterminer que le GIL était la cause du sommeil, et non time.sleep



, par exemple.



La combinaison des uprobes avec perf vous permet de tracer l'appel et le retour des résultats de fonction take_gil



et d' obtenir drop_gil



encore plus d'informations sur l'impact du GIL sur votre programme Python.



Notre travail est facilité par le package expérimental per4m, qui convertit le script perf au format JSON VizTracer, ainsi que certains outils d'orchestration.



Beaucoup de bukaf, n'a pas maîtrisé



Si vous voulez juste voir l'impact du GIL, exécutez ceci une fois:



sudo yum install perf
sudo sysctl kernel.perf_event_paranoid=-1
sudo mount -o remount,mode=755 /sys/kernel/debug
sudo mount -o remount,mode=755 /sys/kernel/debug/tracing
sudo perf probe -f -x `which python` python:take_gil=take_gil
sudo perf probe -f -x `which python` python:take_gil=take_gil%return
sudo perf probe -f -x `which python` python:drop_gil=drop_gil
sudo perf probe -f -x `which python` python:drop_gil=drop_gil%return
pip install "viztracer>=0.11.2" "per4m>=0.1,<0.2"

      
      





Exemple d'utilisation:



# module
$ giltracer per4m/example2.py
# script
$ giltracer -m per4m.example2
# add thread/process state detection
$ giltracer --state-detect -m per4m.example2
# without uprobes (in case that fails)
$ giltracer --state-detect --no-gil-detect -m per4m.example2

      
      





Plans futurs



J'aurais aimé ne pas avoir à développer ces outils. J'espère avoir pu inspirer quelqu'un à créer de meilleurs produits que les miens. Je veux me concentrer sur l'écriture de code haute performance. Cependant, j'ai de tels projets pour l'avenir:






All Articles