Suporte do Python ao perfilador perf
do Linux¶
- autor:
Pablo Galindo
O perfilador perf do Linux é uma ferramenta muito poderosa que permite criar perfis e obter informações sobre o desempenho da sua aplicação. perf
também possui um ecossistema muito vibrante de ferramentas que auxiliam na análise dos dados que produz.
O principal problema de usar o perfilador perf
com aplicações Python é que perf
apenas obtém informações sobre símbolos nativos, ou seja, os nomes de funções e procedimentos escritos em C. Isso significa que os nomes de funções Python e seus nomes de arquivos em seu código não aparecerão na saída de perf
.
Desde o Python 3.12, o interpretador pode ser executado em um modo especial que permite que funções do Python apareçam na saída do criador de perfilador perf
. Quando este modo está habilitado, o interpretador interporá um pequeno pedaço de código compilado instantaneamente antes da execução de cada função Python e ensinará perf
a relação entre este pedaço de código e a função Python associada usando arquivos de mapa perf.
Nota
O suporte para o perfilador perf
está atualmente disponível apenas para Linux em arquiteturas selecionadas. Verifique a saída da etapa de construção configure
ou verifique a saída de python -m sysconfig | grep HAVE_PERF_TRAMPOLINE
para ver se o seu sistema é compatível.
Por exemplo, considere o seguinte script:
def foo(n):
result = 0
for _ in range(n):
result += 1
return result
def bar(n):
foo(n)
def baz(n):
bar(n)
if __name__ == "__main__":
baz(1000000)
Podemos executar perf
para obter amostras de rastreamentos de pilha da CPU em 9999 hertz:
$ perf record -F 9999 -g -o perf.data python my_script.py
Então podemos usar perf report
para analisar os dados:
$ perf report --stdio -n -g
# Children Self Samples Command Shared Object Symbol
# ........ ........ ............ .......... .................. ..........................................
#
91.08% 0.00% 0 python.exe python.exe [.] _start
|
---_start
|
--90.71%--__libc_start_main
Py_BytesMain
|
|--56.88%--pymain_run_python.constprop.0
| |
| |--56.13%--_PyRun_AnyFileObject
| | _PyRun_SimpleFileObject
| | |
| | |--55.02%--run_mod
| | | |
| | | --54.65%--PyEval_EvalCode
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | |
| | | |--51.67%--_PyEval_EvalFrameDefault
| | | | |
| | | | |--11.52%--_PyLong_Add
| | | | | |
| | | | | |--2.97%--_PyObject_Malloc
...
Como você pode ver, as funções Python não são mostradas na saída, apenas _PyEval_EvalFrameDefault
(a função que avalia o bytecode Python) aparece. Infelizmente isso não é muito útil porque todas as funções Python usam a mesma função C para avaliar bytecode, portanto não podemos saber qual função Python corresponde a qual função de avaliação de bytecode.
Em vez disso, se executarmos o mesmo experimento com o suporte perf
ativado, obteremos:
$ perf report --stdio -n -g
# Children Self Samples Command Shared Object Symbol
# ........ ........ ............ .......... .................. .....................................................................
#
90.58% 0.36% 1 python.exe python.exe [.] _start
|
---_start
|
--89.86%--__libc_start_main
Py_BytesMain
|
|--55.43%--pymain_run_python.constprop.0
| |
| |--54.71%--_PyRun_AnyFileObject
| | _PyRun_SimpleFileObject
| | |
| | |--53.62%--run_mod
| | | |
| | | --53.26%--PyEval_EvalCode
| | | py::<module>:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::baz:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::bar:/src/script.py
| | | _PyEval_EvalFrameDefault
| | | PyObject_Vectorcall
| | | _PyEval_Vector
| | | py::foo:/src/script.py
| | | |
| | | |--51.81%--_PyEval_EvalFrameDefault
| | | | |
| | | | |--13.77%--_PyLong_Add
| | | | | |
| | | | | |--3.26%--_PyObject_Malloc
Como habilitar o suporte a perfilação com perf
¶
O suporte à perfilação com perf
pode ser habilitado desde o início usando a variável de ambiente PYTHONPERFSUPPORT
ou a opção -X perf
, ou dinamicamente usando sys.activate_stack_trampoline()
e sys.deactivate_stack_trampoline()
.
As funções sys
têm precedência sobre a opção -X
, a opção -X
tem precedência sobre a variável de ambiente.
Exemplo usando a variável de ambiente:
$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python script.py
$ perf report -g -i perf.data
Exemplo usando a opção -X
:
$ perf record -F 9999 -g -o perf.data python -X perf script.py
$ perf report -g -i perf.data
Exemplo usando as APIs de sys
em example.py
:
import sys
sys.activate_stack_trampoline("perf")
do_profiled_stuff()
sys.deactivate_stack_trampoline()
non_profiled_stuff()
… então:
$ perf record -F 9999 -g -o perf.data python ./example.py
$ perf report -g -i perf.data
Como obter os melhores resultados¶
Para melhores resultados, Python deve ser compilado com CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
, pois isso permite que os perfiladores façam o desenrolamento de pilha (ou stack unwinding) usando apenas o ponteiro de quadro e não no DWARF informações de depuração. Isso ocorre porque como o código interposto para permitir o suporte perf
é gerado dinamicamente, ele não possui nenhuma informação de depuração DWARF disponível.
Você pode verificar se o seu sistema foi compilado com este sinalizador executando:
$ python -m sysconfig | grep 'no-omit-frame-pointer'
Se você não vir nenhuma saída, significa que seu interpretador não foi compilado com ponteiros de quadro e, portanto, pode não ser capaz de mostrar funções Python na saída de perf
.
How to work without frame pointers¶
If you are working with a Python interpreter that has been compiled without
frame pointers, you can still use the perf
profiler, but the overhead will be
a bit higher because Python needs to generate unwinding information for every
Python function call on the fly. Additionally, perf
will take more time to
process the data because it will need to use the DWARF debugging information to
unwind the stack and this is a slow process.
To enable this mode, you can use the environment variable
PYTHON_PERF_JIT_SUPPORT
or the -X perf_jit
option,
which will enable the JIT mode for the perf
profiler.
Nota
Due to a bug in the perf
tool, only perf
versions higher than v6.8
will work with the JIT mode. The fix was also backported to the v6.7.2
version of the tool.
Note that when checking the version of the perf
tool (which can be done
by running perf version
) you must take into account that some distros
add some custom version numbers including a -
character. This means
that perf 6.7-3
is not necessarily perf 6.7.3
.
When using the perf JIT mode, you need an extra step before you can run perf
report
. You need to call the perf inject
command to inject the JIT
information into the perf.data
file.:
$ perf record -F 9999 -g --call-graph dwarf -o perf.data python -Xperf_jit my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data
or using the environment variable:
$ PYTHON_PERF_JIT_SUPPORT=1 perf record -F 9999 -g --call-graph dwarf -o perf.data python my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf report -g -i perf.jit.data
perf inject --jit
command will read perf.data
,
automatically pick up the perf dump file that Python creates (in
/tmp/perf-$PID.dump
), and then create perf.jit.data
which merges all the
JIT information together. It should also create a lot of jitted-XXXX-N.so
files in the current directory which are ELF images for all the JIT trampolines
that were created by Python.
Aviso
Notice that when using --call-graph dwarf
the perf
tool will take
snapshots of the stack of the process being profiled and save the
information in the perf.data
file. By default the size of the stack dump
is 8192 bytes but the user can change the size by passing the size after
comma like --call-graph dwarf,4096
. The size of the stack dump is
important because if the size is too small perf
will not be able to
unwind the stack and the output will be incomplete. On the other hand, if
the size is too big, then perf
won’t be able to sample the process as
frequently as it would like as the overhead will be higher.