Есть много статей, объясняющих, почему 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
Хотелось бы, чтобы я не должен был разрабатывать эти инструменты, и надеюсь, что вдохновляю других, чтобы построить лучшую инструмент, что умоляет мой. Я хочу писать высокопроизводительный код и сосредоточиться на этом.
Тем не менее, я вижу некоторые варианты, которые я планирую написать в будущем:
- Вызовите счетчик производительности оборудования в Viztraacer, чтобы увидеть, например, кэш-пропустить кэш или заметку CPU.
- Введите штабел Python в Perf Stacktlaces, поэтому мы можем объединить их с инструментами из http://www.brendangreg.com/ .например. http://www.brendangregg.com/offcpuanalysis.html
- Повторите те же упражнение, используя DTraace для использования под MacOS.
- Автоматически определяет, какая функция C делает не Отпустите GIL (в основном AutodeTect https://github.com/vaexio/vaex/pull/1114 https://github.com/apache/arrow/pull/7756 )
- Примените их к большему количеству проблем, как в https://github.com/h5py/h5py/issues/1516
Если у вас есть другие идеи, хотите забрать часть этого, оставьте сообщение или свяжитесь со мной.
Примечание: https://github.com/maartenbreddels/per4m находится под разрешенной лицензией MIT, не стесняйтесь использовать это!
Я предполагаю, что Cpython. ↩
Apache Arrow – это зависимость VAEX, поэтому в любое время GIL не выделяется в стрелке, мы (а другие) страдают от удара производительности. ↩
Кроме использования опроса https://github.com/chrisjbillington/gil_load/ ↩
Оригинал: “https://dev.to/maartenbreddels/tracing-and-visualizing-the-python-gil-with-perf-and-viztracer-2jpp”