Рубрики
Без рубрики

Отслеживание и визуализация Python Gil с Perf и Viztracer

Почему Гил имеет значение, есть множество статей, объясняющих, почему Python Gil (глобальный … помеченный с Perf, Python, Gil, Tracing.

Есть много статей, объясняющих, почему Python Gil (глобальный замок переводчика) существует 1 И почему это там. Версия TLDR – это: GIL предотвращает многопоточный чистый код Python с использованием нескольких сердечников CPU.

Однако в VAEX Мы выполняем большую часть деталей CPU интенсивных частей в C (C ++), где мы выпустим Gil. Это обычная практика в высокопроизводительных библиотеках Python, где Python действует просто как клей высокого уровня.

Тем не менее, GIL необходимо выпустить явно, и это ответственность программиста и может быть забыто, что привело к неоптимальному использованию вашей машины.

Я недавно имел этот вопрос в VAEX Где я просто забыл освободить Гил и нашел подобную проблему в Apache arrow 2 Отказ

Также при запуске на 64 ядра я иногда вижу производительность в VAEX, с которым я не доволен. Это может использоваться 4000% процессора, а не 6400% процессора, что я не доволен. Вместо слепо натягивая некоторые рычаги, чтобы осмотреть эффект, я хочу понять, что происходит, и если GIL – это проблема, почему и где он держит VAEX.

Я планирую написать серию статей, объясняющих некоторые инструменты и методы, доступные для профилирования/отслеживания Python вместе с нативными расширениями, и как эти инструменты могут быть склеены, проанализировать и визуализировать, что делает Python, и когда Гил или упал.

Я надеюсь, что это приводит к улучшению отслеживания, профилирования и других характеристик производительности в экосистеме Python и производительность всей экосистемы Python.

Linux.

Получите доступ к машине Linux и убедитесь, что у вас есть корневые привилегии (Sudo в порядке), или попросите вашего SYSADMIN выполнить некоторые из этих команд для вас. Для остальной части документа мы бежим только как пользователь.

Делать

Убедитесь, что у вас установлены все, например, на Ubuntu:

$ sudo yum install perf

Конфигурация ядра

Чтобы включить его как пользователь:

# 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

Пакеты Python

Мы будем использовать Вицтрацер и пер4 м

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

Там нет способа получить государство GIL в Python 3 Поскольку для этого нет API. Мы можем отслеживать его из ядра, а нужный инструмент для этого под Linux является делать .

Использование Linux Perf Tool (aka perf_events) мы можем прослушать изменения состояния для процессов/потоков (мы заботимся только о спящем и беге) и в системе их. Хотя Perf может выглядеть страшно, это мощный инструмент. Если вы хотите узнать немного больше о Perf, я рекомендую читать Zine Zine Evans Zine On Perf или пройти через сайт Брендан Грегга .

Чтобы построить нашу интуицию, мы сначала заберем Perf на Очень тривиальная программа :

# file per4m/example0.py
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()

Мы слушаем всего несколько событий, чтобы сохранить шум (обратите внимание на использование подстановочных знаков):

$ 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) ]

И использовать Perf Script Команда для записи человека/разборных выходов.

$ 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

Найдите минутку, чтобы переваривать вывод. Я вижу несколько вещей. Глядя на 4-й столбец (время в секундах), мы видим, где проспал программу (она пропускает 1 секунду). Здесь мы видим, что мы вводим сонное состояние с помощью линии, такими как:

Python 3040114. [031] 5563911.112513: Очистите: SCEEL_SWICH: ==>/31

Это означает, что ядро изменило состояние потока Python в S ) государство.

Полная секунда позже мы видим, что он проснулся:

Swapper 0. [031] 5563912.113697: Очистка: SCEN_WAKEUP:

Конечно, вам нужно построить некоторые инструменты вокруг этого, чтобы понять, что происходит. Но можно представить, что этот вывод может быть легко проанализирован, и это то, что Per4m делает. Однако, прежде чем мы пойдем туда, я сначала хотел бы визуализировать поток немного более продвинутой программы, используя Вицтрацер Отказ

Вицтрацер Является ли Python Tracer, который может визуализировать то, что ваша программа делает в браузере. Давайте запустим его немного Более сложный пример чтобы увидеть, как это выглядит.

# file per4m/example1.py
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()

Запуск Viztraacer дает вывод, как:

$ 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.

И HTML должен рендер, так как:

Из этого похоже, что Некоторые_computation Кажется, казнены параллельно (дважды), в то время как на самом деле мы знаем, что Гил предотвращает это. Так что на самом деле происходит?

Давайте запустим Perf На этом, аналогично тому, что мы сделали с примером0.пи. Однако мы добавляем аргумент -k clock_monotonic так что мы используем Те же часы, что и визатрацер И попросите Viztraacer генерировать JSON вместо 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

Тогда мы используем Per4m Чтобы перевести результаты перфориса в JSON, что Viztraacer может прочитать

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

Затем используйте Viztraacer для объединения двух файлов 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.

Это дает нам:

Из этой визуализации ясно, что резьбы регулярно входят в сонное состояние из-за GIL и не выполняют параллельно.

Примечание. Длина фазы сна составляет ~ 5 мм, что соответствует значению по умолчанию sys.getswitchInterval.

Мы видим наш процесс спящим, но мы не видим никакой разницы между сонным состоянием, вызванным вызывающим Time.sleep И из-за Гила. Есть несколько способов увидеть разницу, и мы представим два метода.

Через следы стека

Использование Perf Record -G (или лучше Perf Record - Call-граф карликовый который подразумевает -G ), мы получаем следы стека для каждого события 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) ]

Глядя на вывод Perf Script (Где мы добавляем --NO INLINE по причинам эффективности), мы получаем нагрузку на информацию. Глядя на событие изменить состояние, теперь мы можем увидеть это take_gil назывался!

$ 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 pthread_cond_timedwait@@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)
...

Примечание: мы также видим, что pthread_cond_timedwait называется, это то, что https://github.com/sumerc/gilstats.py . Использует для инструмента EBPF, если вы заинтересованы в других мьютексах.

Примечание. Также обратите внимание, что мы не видим Python Stacktrace, но _Pyeval_evalframedefault Etcetera вместо этого. Я планирую написать, как вводить укладки Python в будущей статье.

Per4m Perf2Trace Преобразовать инструмент понимает это и будет генерировать разные вывода, когда 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.

Это дает нам:

Теперь мы действительно видим, где Gil играет роль!

Через зонды (KPROBES/UPROBES)

Теперь мы знаем, когда процессы спят (из-за GIL или других причин), но если мы хотим получить более подробный взгляд, где GIL взял или сброшен, нам нужно знать, где take_gil и Drop_Gil называются, а также возвращены. Это можно проследить с помощью рублей через Perf. Рубцы – это зонды в юности, эквивалентные KPROBES, которые, как вы, возможно, догадались, работают в пространстве ядра. Джулия Эванс опять же отличный ресурс.

Давайте установим 4 рубцы:

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

Это немного жалуется и, кажется, добавляет несколько зондов/событий, потому что Drop_Gil и take_gil включены (что означает, что функция присутствует несколько раз в двоичном двоике), но, похоже, работает 🤷 (Дайте мне знать в комментариях, если это не работает для вас).

ПРИМЕЧАНИЕ. Мне, возможно, повезло, что бинарный питон, который я использую (от Conda-Forge), скомпилирован таким образом, чтобы соответствующий Take_GIL/Drop_GIL (и его возврат), которые добиваются успеха, являются соответствующими для этой проблемы.

Обратите внимание, что датчики не приводят к тому, что они не являются ударами производительности, только когда они являются «активными» (вроде бы, когда мы отслеживаем их под Perf), будет нажатие другой путь кода. При контроле, пострадавшие страницы для контролируемого процесса будут скопированы, а точки останова вставляются в правильных местах (INT3 для процессоров x86). Точка останова заставит событие для Perf, что вызывает небольшой накладной расход. Если вы хотите удалить зонды, запустите:

$ sudo perf probe --del 'python*'

Сейчас Perf Понять новые события, которые он может слушать, поэтому давайте снова запустимся с -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

Примечание: мы удалили --call-граф карлик В противном случае Perf не может идти в ногу, и мы потеряем события.

Затем мы используем Per4m Perf2Trace Чтобы преобразовать это в JSON, что Viztraacer понимает, пока мы также получаем бесплатную статистику.

$ 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

Обратите внимание, что Per4m Perf2Trace Gil Подкоманда также дает gil_load как вывод. С этого вывода мы видим, что оба потока ждут GIL примерно 50% времени, как и ожидалось.

Используя то же самое perf.data Файл, что Perf Record Сгенерировано, мы также можем генерировать информацию о состоянии потока/процесса. Тем не менее, потому что мы бежали без штабел, мы не знаем, сначаемся ли мы из-за Gil или нет.

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

Наконец, мы объединяем три выхода:

$ 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.

Наш выход Viztraacer дает нам хороший обзор того, кто имеет, и хочет, чтобы Гил:

Над каждой нитью мы видим, когда нить/процесс хочет взять GIL, а когда он преуспел (отмечен Замок ). Обратите внимание, что эти периоды перекрываются с периодами, когда нить/процесс – не Спать (так работает!). Также обратите внимание, что мы видим только 1 нить/процесс в запущенном состоянии, как и ожидалось, из-за GIL.

Время между каждым вызовом на take_gil и на самом деле получение блокировки (и, таким образом, оставляя сонное состояние, или просыпаться), это ровно время в приведенной выше таблице в столбце Gil ждать% ❌ . Время каждого потока имеет Гил, или время, охватываемое ЗАМОК , это время в колонне имеет gil% ❗ .

Мы увидели чистую программу Python, запущенную многопотативную программу, где GIL ограничивает производительность, позволяя только 1 поток/процесс запускаться за раз 1 Отказ Давайте теперь посмотрим, что произойдет, когда код выпускает GIL, например, что происходит, когда мы выполняем numpy функции.

Второй пример выполняется quey_numpy_computation , который вызывает функцию numpy M = 4 Время, параллельно, используя 2 потока, в то время как основной нить выполняет чистый код Python.

# file per4m/example3.py
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()

Вместо того, чтобы запустить этот скрипт, используя Perf и Viztraacer, мы теперь используем Per4m Giltracer UTIL, что автоматизирует все шаги, выполненные выше (в немного более умнее 1 ):

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

Summary of threads:

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 ...
...

Мы видим, что, в то время как основной нить выполняет код Python (он имеет GIL, указывающую Блокировка над ним), нити также работают. Обратите внимание, что в примере чистого Python у нас был только один поток/процесс, работающий одновременно. Хотя здесь мы видим моменты, где 3 нити действительно бегают параллельно). Это возможно, потому что Numpy-процедуры, которые входят в C/C ++/Fortran, выпустили GIL.

Тем не менее, темы все еще мешают GIL, так как как только функция Numpy возвращает на землю Python, она должна снова получить GIL, так как можно увидеть take_gil блоки много времени. Это приводит к тому, что время ожидания 10% для каждого потока.

Так как мой рабочий процесс часто включает в себя работу с ноутбука MacBook 1 Удаленно подключенный к компьютеру Linux, я использую ноутбук Jupyter часто для удаленного выполнения кода. Будучи разработчиком Jupyter, создавая клеточную магию, чтобы обернуть это, было обязательным.

#
# 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.

Скачать giltracer.html.

Откройте Giltracer.html на новой вкладке (не может работать из-за проблемы безопасности)

Используя Perf, мы можем определить состояния процесса/потока, которые дают нам идею, какую нить/процесс имеет GIL в Python. Используя укладки, мы можем узнать, если сонные состояния действительно связаны с GIL, а не из-за Time.sleep например.

Сочетая рубцы с Perf, мы можем проследить вызов и возвращение take_gil и Drop_Gil Функции, получая еще лучшее представление о влиянии Gil в вашей программе Python.

Per4m Python Package, это экспериментальная упаковка, чтобы сделать некоторые из Perf Script к формату Viztraacer JSON, а также некоторые инструменты оркестрации, с которыми облегчают работу.

Если вы просто хотите увидеть, куда делают Гил:

Запустите это один раз:

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"

Пример использования:

# 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

Хотелось бы, чтобы я не должен был разрабатывать эти инструменты, и надеюсь, что вдохновляю других, чтобы построить лучшую инструмент, что умоляет мой. Я хочу писать высокопроизводительный код и сосредоточиться на этом.

Тем не менее, я вижу некоторые варианты, которые я планирую написать в будущем:

Если у вас есть другие идеи, хотите забрать часть этого, оставьте сообщение или свяжитесь со мной.

Примечание: https://github.com/maartenbreddels/per4m находится под разрешенной лицензией MIT, не стесняйтесь использовать это!

  1. Я предполагаю, что Cpython. ↩

  2. Apache Arrow – это зависимость VAEX, поэтому в любое время GIL не выделяется в стрелке, мы (а другие) страдают от удара производительности. ↩

  3. Кроме использования опроса https://github.com/chrisjbillington/gil_load/

Оригинал: “https://dev.to/maartenbreddels/tracing-and-visualizing-the-python-gil-with-perf-and-viztracer-2jpp”