Die Ausgabe des Linux-Leistungsberichts verstehen

Lesezeit: 7 Minuten

Obwohl ich die meisten Ergebnisse intuitiv erhalten kann, fällt es mir schwer, die Ausgabe von vollständig zu verstehen perf report Befehl speziell für den Anrufgraphen, also habe ich einen dummen Test geschrieben, um dieses Problem von mir ein für alle Mal zu lösen.

Der dumme Test

Folgendes habe ich zusammengestellt mit:

gcc -Wall -pedantic -lm perf-test.c -o perf-test

Keine aggressiven Optimierungen zur Vermeidung von Inlining und dergleichen.

#include <math.h>

#define N 10000000UL

#define USELESSNESS(n)                          \
    do {                                        \
        unsigned long i;                        \
        double x = 42;                          \
        for (i = 0; i < (n); i++) x = sin(x);   \
    } while (0)

void baz()
{
    USELESSNESS(N);
}

void bar()
{
    USELESSNESS(2 * N);
    baz();
}

void foo()
{
    USELESSNESS(3 * N);
    bar();
    baz();
}

int main()
{
    foo();
    return 0;
}

Flache Profilierung

perf record ./perf-test
perf report

Mit diesen bekomme ich:

  94,44%  perf-test  libm-2.19.so       [.] __sin_sse2
   2,09%  perf-test  perf-test          [.] sin@plt
   1,24%  perf-test  perf-test          [.] foo
   0,85%  perf-test  perf-test          [.] baz
   0,83%  perf-test  perf-test          [.] bar

Was vernünftig klingt, da die schwere Arbeit tatsächlich von durchgeführt wird __sin_sse2 und sin@plt ist wahrscheinlich nur ein Wrapper, während der Overhead meiner Funktionen insgesamt nur die Schleife berücksichtigt: 3*N Iterationen für foo, 2*N für die anderen beiden.

Hierarchisches Profiling

perf record -g ./perf-test
perf report -G
perf report

Jetzt sind die Overhead-Spalten, die ich bekomme, zwei: Children (die Ausgabe wird standardmäßig danach sortiert) und Self (gleicher Overhead des Flachprofils).

Hier beginne ich zu fühlen, dass ich etwas vermisse: unabhängig davon, dass ich es benutze -G oder nicht, ich kann die Hierarchie nicht in Bezug auf “x ruft y” oder “y wird von x angerufen” erklären, zum Beispiel:

  • ohne -G (“y wird von x aufgerufen”):

    -   94,34%    94,06%  perf-test  libm-2.19.so       [.] __sin_sse2
       - __sin_sse2
          + 43,67% foo
          + 41,45% main
          + 14,88% bar
    -   37,73%     0,00%  perf-test  perf-test          [.] main
         main
         __libc_start_main
    -   23,41%     1,35%  perf-test  perf-test          [.] foo
         foo
         main
         __libc_start_main
    -    6,43%     0,83%  perf-test  perf-test          [.] bar
         bar
         foo
         main
         __libc_start_main
    -    0,98%     0,98%  perf-test  perf-test          [.] baz
       - baz
          + 54,71% foo
          + 45,29% bar
    
    1. Warum __sin_sse2 wird von angerufen main (indirekt?), foo und bar aber nicht durch baz?
    2. Warum Funktionen manchmal einen Prozentsatz und eine Hierarchie angehängt haben (z. B. die letzte Instanz von baz) und manchmal nicht (z. B. die letzte Instanz von bar)?
  • mit -G (“x ruft y an”):

    -   94,34%    94,06%  perf-test  libm-2.19.so       [.] __sin_sse2
       + __sin_sse2
       + __libc_start_main
       + main
    -   37,73%     0,00%  perf-test  perf-test          [.] main
       - main
          + 62,05% foo
          + 35,73% __sin_sse2
            2,23% sin@plt
    -   23,41%     1,35%  perf-test  perf-test          [.] foo
       - foo
          + 64,40% __sin_sse2
          + 29,18% bar
          + 3,98% sin@plt
            2,44% baz
         __libc_start_main
         main
         foo
    
    1. Wie soll ich die ersten drei Einträge unter interpretieren __sin_sse2?
    2. main Anrufe foo und das ist ok, aber warum, wenn es anruft __sin_sse2 und sin@plt (indirekt?) es ruft auch nicht an bar und baz?
    3. Warum machen __libc_start_main und main unter erscheinen foo? Und warum foo taucht zweimal auf?

Vermutlich gibt es zwei Ebenen dieser Hierarchie, in denen die zweite tatsächlich die Semantik “x ruft y”https://stackoverflow.com/”y wird von x aufgerufen” darstellt, aber ich bin müde zu raten, also ich’ Ich frage hier. Und die Dokumentation scheint nicht zu helfen.


Entschuldigung für den langen Beitrag, aber ich hoffe, dass all dieser Kontext auch jemand anderem helfen oder als Referenz dienen kann.

  • Ich bin kein Experte für perf, aber ich weiß, dass es standardmäßig ~ 1000 Mal pro Sekunde auf den Stapel schaut, um seine Daten zu sammeln. Daher wird eine feinkörnige Analyse, wie Sie es versuchen, wahrscheinlich fehlschlagen. So ist es zB möglich, dass keines der Samples wann aufgetreten ist sin_sse2 wurde angerufen baz. Erwägen Sie die Verwendung gprofdas in Stubs kompiliert wird, um jeden Anruf abzufangen und zurückzugeben (obwohl es andere Probleme hat).

    – Gen

    2. Januar 2015 um 16:34 Uhr

  • Ja, ich weiß, aber es ist schnell und ließ mich alle möglichen verrückten Ereignisse wie Cache-Fehlschläge und falsche Verzweigungsvorhersage pro Symbol aufzeichnen, während AFAIK gprof kann es nicht. ich benutzte N ziemlich groß, genau um das zu vermeiden, was Sie erwähnt haben; Jedenfalls habe ich ohne Glück versucht, es noch weiter zu erhöhen, ich weiß es nicht, aber ich denke, dass es ziemlich unwahrscheinlich ist, dass selbst in einer 100M-Iterationsschleife keine Samples gesammelt werden.

    – cYrus

    2. Januar 2015 um 17:35 Uhr


Benutzer-Avatar
Matthäus G.

Okay, gut, lassen Sie uns vorübergehend den Unterschied zwischen Caller- und Callee-Call-Graphen ignorieren, vor allem, weil ich beim Vergleich der Ergebnisse zwischen diesen beiden Optionen auf meinem Computer nur Effekte innerhalb von sehe kernel.kallsyms DSO aus Gründen, die ich nicht verstehe – ich selbst bin relativ neu dafür.

Ich fand, dass es für Ihr Beispiel etwas einfacher ist, den gesamten Baum zu lesen. Also verwenden --stdioschauen wir uns den ganzen Baum an __sin_sse2:

# Overhead    Command      Shared Object                  Symbol
# ........  .........  .................  ......................
#
    94.72%  perf-test  libm-2.19.so       [.] __sin_sse2
            |
            --- __sin_sse2
               |
               |--44.20%-- foo
               |          |
               |           --100.00%-- main
               |                     __libc_start_main
               |                     _start
               |                     0x0
               |
               |--27.95%-- baz
               |          |
               |          |--51.78%-- bar
               |          |          foo
               |          |          main
               |          |          __libc_start_main
               |          |          _start
               |          |          0x0
               |          |
               |           --48.22%-- foo
               |                     main
               |                     __libc_start_main
               |                     _start
               |                     0x0
               |
                --27.84%-- bar
                          |
                           --100.00%-- foo
                                     main
                                     __libc_start_main
                                     _start
                                     0x0

Ich lese das also so: 44 % der Zeit sin wird von angerufen foo; 27 % der Zeit, von der aus angerufen wird bazund 27% von bar.

Die Dokumentation für -g ist aufschlussreich:

 -g [type,min[,limit],order[,key]], --call-graph
       Display call chains using type, min percent threshold, optional print limit and order. type can be either:

       ·   flat: single column, linear exposure of call chains.

       ·   graph: use a graph tree, displaying absolute overhead rates.

       ·   fractal: like graph, but displays relative rates. Each branch of the tree is considered as a new profiled object.

               order can be either:
               - callee: callee based call graph.
               - caller: inverted caller based call graph.

               key can be:
               - function: compare on functions
               - address: compare on individual code addresses

               Default: fractal,0.5,callee,function.

Das Wichtige dabei ist, dass die Voreinstellung fraktal ist, und im fraktalen Modus ist jeder Zweig ein neues Objekt.

Das sieht man also in 50% der Fälle baz heißt, es heißt von barund die anderen 50 % werden aufgerufen foo.

Dies ist nicht immer die nützlichste Maßnahme, daher ist es aufschlussreich, die Ergebnisse mit zu betrachten -g graph:

94.72%  perf-test  libm-2.19.so       [.] __sin_sse2
        |
        --- __sin_sse2
           |
           |--41.87%-- foo
           |          |
           |           --41.48%-- main
           |                     __libc_start_main
           |                     _start
           |                     0x0
           |
           |--26.48%-- baz
           |          |
           |          |--13.50%-- bar
           |          |          foo
           |          |          main
           |          |          __libc_start_main
           |          |          _start
           |          |          0x0
           |          |
           |           --12.57%-- foo
           |                     main
           |                     __libc_start_main
           |                     _start
           |                     0x0
           |
            --26.38%-- bar
                      |
                       --26.17%-- foo
                                 main
                                 __libc_start_main
                                 _start
                                 0x0

Dies ändert sich zur Verwendung absoluter Prozentsätze, bei denen jeder Prozentsatz der Zeit für diese Anrufkette gemeldet wird: Also foo->bar beträgt 26 % der gesamten Ticks (die wiederum callen baz), und foo->baz (direkt) beträgt 12 % der gesamten Ticks.

Ich habe immer noch keine Ahnung, warum ich aus der Perspektive von keine Unterschiede zwischen Callee- und Caller-Graphen sehe __sin_sse2.

Aktualisieren

Eine Sache, die ich von Ihrer Befehlszeile aus geändert habe, ist, wie die Callgraphs gesammelt wurden. Linux perf verwendet standardmäßig die Frame-Pointer-Methode zum Rekonstruieren von Callstacks. Dies kann ein Problem sein, wenn der Compiler verwendet -fomit-frame-pointer Als ein Ursprünglich. Also habe ich verwendet

perf record --call-graph dwarf ./perf-test

  • Die Sache mit den relativen vs. absoluten Prozentsätzen macht absolut Sinn. Der Hauptpunkt ist, dass ich einfach keine Ausgabe finden kann, die Ihrer ähnlich ist (was seitdem legitim erscheint __sin_sse2 scheint angerufen zu werden direkt von allen drei Funktionen, während meine Funktionen foo, main und bar nur). Können Sie bitte die genauen Flags posten, die Sie verwendet haben?

    – cYrus

    4. Februar 2015 um 16:38 Uhr

  • @cYrus werfen Sie einen Blick auf mein Update … Wird die genaue Befehlszeile für posten perf report nach dem Mittagessen.

    – Matthäus G.

    4. Februar 2015 um 17:22 Uhr

  • @MatthewG. Nun, es sieht so aus, als ginge es nur um die Verwendung dwarf… Ich werde prüfen, ob das alle meine aufgeführten Probleme löst.

    – cYrus

    4. Februar 2015 um 17:34 Uhr

  • @cYrus Ich habe Ihre Befehlszeile zum Kompilieren kopiert und eingefügt (musste jedoch das Linker-Flag an das Ende verschieben). Kernel ist 3.13.0-37-generisch (was kann Angelegenheit). Die Familie der Befehlszeilen für Berichte war perf report -g {graph,fractal},0.05,calle{e,r} --stdio > {graph,fracal}Calle{e,r}

    – Matthäus G.

    4. Februar 2015 um 17:46 Uhr

  • Sind Sie sicher, dass die Prozentsätze die Anzahl der Aufrufe und nicht die Anzahl der Zyklen widerspiegeln?

    – Nathan B

    27. August 2018 um 11:32 Uhr

1370220cookie-checkDie Ausgabe des Linux-Leistungsberichts verstehen

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy