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 python script.py
$ perf report -g -i perf.data
Exemplo usando a opção -X
:
$ 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:
$ 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
.