Python-stöd för Linux-profileraren perf

författare:

Pablo Galindo

Linux perf profiler är ett mycket kraftfullt verktyg som gör att du kan profilera och få information om prestandan i din applikation. perf har också ett mycket levande ekosystem av verktyg som hjälper till med analysen av de data som den producerar.

Det största problemet med att använda profileraren perf med Python-program är att perf bara får information om inbyggda symboler, det vill säga namnen på funktioner och procedurer skrivna i C. Det innebär att namnen och filnamnen på Python-funktioner i din kod inte kommer att visas i utdata från perf.

Sedan Python 3.12 kan tolken köras i ett speciellt läge som gör att Python-funktioner kan visas i utdata från profileraren perf. När detta läge är aktiverat kommer tolken att lägga in en liten kodbit som kompileras i farten före exekveringen av varje Python-funktion och den kommer att lära perf förhållandet mellan denna kodbit och den associerade Python-funktionen med hjälp av perf map files.

Anteckning

Stöd för profileraren perf finns för närvarande endast för Linux på utvalda arkitekturer. Kontrollera utdata från byggsteget configure eller kontrollera utdata från python -m sysconfig | grep HAVE_PERF_TRAMPOLINE för att se om ditt system stöds.

Tänk till exempel på följande skript:

def foo(n):
    resultat = 0
    för _ i intervall(n):
        resultat += 1
    returnera resultat

def bar(n):
    foo(n)

def baz(n):
    bar(n)

om __name__ == "__main__":
    baz(1000000)

Vi kan köra perf för att prova CPU-stackspårningar vid 9999 hertz:

$ perf record -F 9999 -g -o perf.data python my_script.py

Sedan kan vi använda perf report för att analysera data:

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

Som du kan se visas inte Python-funktionerna i utdata, bara _PyEval_EvalFrameDefault (funktionen som utvärderar Python-bytekoden) visas. Tyvärr är det inte särskilt användbart eftersom alla Python-funktioner använder samma C-funktion för att utvärdera bytecode, så vi kan inte veta vilken Python-funktion som motsvarar vilken bytecode-utvärderingsfunktion.

Istället, om vi kör samma experiment med perf support aktiverat får vi:

$ 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

Så här aktiverar du profileringsstödet perf

profileringsstödet perf kan aktiveras antingen från början med hjälp av miljövariabeln PYTHONPERFSUPPORT eller alternativet -X perf, eller dynamiskt med hjälp av sys.activate_stack_trampoline() och sys.deactivate_stack_trampoline().

Funktionerna sys har företräde framför alternativet -X, alternativet -X har företräde framför miljövariabeln.

Exempel, med hjälp av miljövariabeln:

$ PYTHONPERFSUPPORT=1 perf record -F 9999 -g -o perf.data python my_script.py
$ perf rapport -g -i perf.data

Exempel på användning av -X option:

$ perf record -F 9999 -g -o perf.data python -X perf my_script.py
$ perf rapport -g -i perf.data

Exempel på användning av API:erna sys i filen example.py:

import sys

sys.activate_stack_trampoline("perf")
do_profiled_stuff()
sys.deactivate_stack_trampoline()

non_profiled_stuff()

…sedan..:

$ perf record -F 9999 -g -o perf.data python ./example.py
$ perf rapport -g -i perf.data

Hur man uppnår bästa resultat

För bästa resultat bör Python kompileras med CFLAGS="-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer" eftersom detta gör det möjligt för profilerare att rulla ut med endast rampekaren och inte på DWARF-felsökningsinformation. Detta beror på att koden som läggs in för att tillåta perf-stöd genereras dynamiskt och därför inte har någon DWARF-felsökningsinformation tillgänglig.

Du kan kontrollera om ditt system har kompilerats med denna flagga genom att köra:

$ python -m sysconfig | grep 'no-omit-frame-pointer'

Om du inte ser någon utmatning betyder det att din tolk inte har kompilerats med rampekare och därför kanske den inte kan visa Python-funktioner i utmatningen från perf.

Hur man arbetar utan rampekare

Om du arbetar med en Python-tolk som har kompilerats utan rampekare kan du fortfarande använda profileraren perf, men omkostnaderna blir lite högre eftersom Python måste generera avrullningsinformation för varje Python-funktionsanrop i farten. Dessutom kommer det att ta längre tid för perf att bearbeta data eftersom den måste använda DWARF-felsökningsinformationen för att spola tillbaka stacken och det är en långsam process.

För att aktivera det här läget kan du använda miljövariabeln PYTHON_PERF_JIT_SUPPORT eller alternativet -X perf_jit, som aktiverar JIT-läget för profileraren perf.

Anteckning

På grund av en bugg i verktyget perf är det bara perf-versioner som är högre än v6.8 som fungerar med JIT-läget. Fixen har även backporterats till v6.7.2-versionen av verktyget.

Observera att när du kontrollerar versionen av verktyget perf (vilket kan göras genom att köra perf version) måste du ta hänsyn till att vissa distros lägger till egna versionsnummer som innehåller tecknet -. Detta innebär att perf 6.7-3 inte nödvändigtvis är perf 6.7.3.

När du använder perf JIT-läget behöver du ett extra steg innan du kan köra perf report. Du måste anropa kommandot perf inject för att injicera JIT-informationen i filen perf.data:

$ perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py
$ perf inject -i perf.data --jit --output perf.jit.data
$ perf rapport -g -i perf.jit.data

eller med hjälp av miljövariabeln:

$ 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 rapport -g -i perf.jit.data

kommandot perf inject --jit kommer att läsa perf.data, automatiskt plocka upp den perf dump-fil som Python skapar (i /tmp/perf-$PID.dump) och sedan skapa perf.jit.data som sammanfogar all JIT-information. Det bör också skapa en hel del jitted-XXXX-N.so-filer i den aktuella katalogen som är ELF-bilder för alla JIT-trampoliner som skapades av Python.

Varning

När du använder --call-graph dwarf kommer verktyget perf att ta ögonblicksbilder av stacken i den process som profileras och spara informationen i filen perf.data. Som standard är storleken på stackdumpen 8192 byte, men du kan ändra storleken genom att ange den efter ett kommatecken som --call-graph dwarf,16384.

Storleken på stackdumpen är viktig, för om den är för liten kan perf inte spola tillbaka stacken och utdata blir ofullständiga. Å andra sidan, om storleken är för stor, kommer perf inte att kunna sampla processen så ofta som den skulle vilja eftersom overhead blir högre.

Stackstorleken är särskilt viktig vid profilering av Python-kod som kompilerats med låga optimeringsnivåer (som -O0), eftersom dessa kompileringar tenderar att ha större stackramar. Om du kompilerar Python med -O0 och inte ser Python-funktioner i din profileringsutdata, försök att öka stackdumpstorleken till 65528 byte (max):

$ perf record -F 9999 -g -k 1 --call-graph dwarf,65528 -o perf.data python -Xperf_jit my_script.py

Olika kompileringsflaggor kan påverka stackstorleken avsevärt:

  • Byggnader med -O0 har vanligtvis mycket större stapelramar än de med -O1 eller högre

  • Att lägga till optimeringar (-O1, -O2, etc.) minskar vanligtvis stackstorleken

  • Frame-pekare (-fno-omit-frame-pointer) ger i allmänhet mer tillförlitlig stackavveckling