Perf überzählen einfache CPU-gebundene Schleife: mysteriöse Kernel-Arbeit?

Ich benutze Linux perf für einige Zeit, um Anwendungsprofile zu erstellen. In der Regel ist die Anwendung mit Profil ziemlich komplex, daher werden die gemeldeten Zählerwerte in der Regel einfach als Nennwert verwendet, sofern kein @ vorhanden isbrutt Diskrepanz mit dem, was Sie aufgrund erster Prinzipien erwarten könnten.

In letzter Zeit habe ich jedoch einige einfache 64-Bit-Assembly-Programme vorgestellt - triival genug, dass man den erwarteten Wert verschiedener Leistungsindikatoren fast genau berechnen kann, und es scheint, dassperf stat zählt zu viel.

Nehmen Sie zum Beispiel die folgende Schleife:

.loop:
    nop
    dec rax
    nop
    jne .loop

Dies wird einfach eine Schleifen mal, won ist der Anfangswert vonrax. Jede Iteration der Schleife führt 4 Anweisungen aus, sodass Sie @ erwarten würde4 * n ausgeführte Anweisungen, plus einige kleine Fixkosten für das Starten und Beenden von Prozessen und das kleine Stück Code, das @ setn vor dem Betreten der Schleife.

Hier ist das (typische)perf stat Ausgabe fürn = 1,000,000,000:

~/dev/perf-test$ perf stat ./perf-test-nop 1

 Performance counter stats for './perf-test-nop 1':

        301.795151      task-clock (msec)         #    0.998 CPUs utilized          
                 0      context-switches          #    0.000 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 2      page-faults               #    0.007 K/sec                  
     1,003,144,430      cycles                    #    3.324 GHz                      
     4,000,410,032      instructions              #    3.99  insns per cycle        
     1,000,071,277      branches                  # 3313.742 M/sec                  
             1,649      branch-misses             #    0.00% of all branches        

       0.302318532 seconds time elapsed

Huh. Anstelle von ungefähr 4.000.000.000 Anweisungen und 1.000.000.000 Zweigen sehen wir mysteriöse zusätzliche 410.032 Anweisungen und 71.277 Zweige. Es gibt immer "zusätzliche" Anweisungen, aber die Menge variiert ein bisschen - nachfolgende Läufe hatten zum Beispiel 421K, 563K und 464Kextr Anweisungen jeweils. Sie können dies auf Ihrem System selbst ausführen, indem Sie my @ erstellesimple github project.

OK, Sie können also davon ausgehen, dass es sich bei diesen wenigen hunderttausend zusätzlichen Anweisungen nur um feste Installations- und Abbaukosten für Anwendungen handelt (das Userland-Setup lautetsehr klei, aber es könnte versteckte Sachen geben). Lass uns versuchen fürn=10 billion dann

~/dev/perf-test$ perf stat ./perf-test-nop 10

 Performance counter stats for './perf-test-nop 10':

       2907.748482      task-clock (msec)         #    1.000 CPUs utilized          
                 3      context-switches          #    0.001 K/sec                  
                 0      cpu-migrations            #    0.000 K/sec                  
                 2      page-faults               #    0.001 K/sec                  
    10,012,820,060      cycles                    #    3.443 GHz                    
    40,004,878,385      instructions              #    4.00  insns per cycle        
    10,001,036,040      branches                  # 3439.443 M/sec                  
             4,960      branch-misses             #    0.00% of all branches        

       2.908176097 seconds time elapsed

Jetzt gibt es ~ 4,9 Millionenextr -Anweisungen, etwa 10-fache Zunahme gegenüber zuvor, proportional zur 10-fachen Zunahme der Schleifenzahl.

Sie können verschiedene Zähler ausprobieren - alle CPU-bezogenen zeigen ähnliche proportionale Erhöhungen. Konzentrieren wir uns dann auf die Anzahl der Anweisungen, um die Dinge einfach zu halten. Verwendung der:u und:k Suffixe zum MessenBenutze und kernel counts zeigt an, dass im kernel Konto für fast alle zusätzlichen Ereignisse:

~/dev/perf-test$ perf stat -e instructions:u,instructions:k ./perf-test-nop 1

 Performance counter stats for './perf-test-nop 1':

     4,000,000,092      instructions:u                                              
           388,958      instructions:k                                              

       0.301323626 seconds time elapsed

Bingo. Von den 389.050 zusätzlichen Anweisungen entfielen 99,98% (388.958) auf den Kernel.

OK, aber wo bleibt uns das? Dies ist eine triviale CPU-gebundene Schleife. Es werden keine Systemaufrufe durchgeführt und es wird nicht auf den Speicher zugegriffen (wodurch der Kernel indirekt über den Seitenfehlermechanismus aufgerufen werden kann). Warum führt der Kernel Anweisungen für meine Anwendung aus?

Es scheint nicht durch Kontextwechsel oder CPU-Migrationen verursacht zu werden, da diese bei oder nahe Null liegen und auf jeden Fall dasextrie Anzahl der @ -Anweisungen korreliert nicht mit Läufen, bei denen mehr dieser Ereignisse auftraten.

Die Anzahl der zusätzlichen Kernel-Anweisungen ist in der Tat mit der Schleifenzahl sehr glatt. Hier ist ein Diagramm mit (Milliarden von) Schleifeniterationen im Vergleich zu Kernel-Anweisungen:

Sie können sehen, dass die Beziehung so ziemlich perfekt linear ist - tatsächlich gibt es bis zu 15e9-Iterationen nur einen Ausreißer. Danach scheinen zwei separate Linien zu existieren, die auf eine Art Quantisierung dessen hindeuten, was die überschüssige Zeit verursacht. In jedem Fall fallen für jede 1e9-Anweisung, die in der Hauptschleife ausgeführt wird, etwa 350.000 Kernel-Anweisungen an.

Zum Schluss habe ich bemerkt, dass die Anzahl der ausgeführten Kernel-Befehle proportional zu @ zu sein scheinLaufzei1 (oder CPU-Zeit) statt ausgeführter Anweisungen. Um dies zu testen, benutze ich einähnliches Programm, aber mit einem dernop Anweisungen durch ein @ ersetidiv mit einer Latenz von ca. 40 Zyklen (einige uninteressante Zeilen entfernt):

~/dev/perf-test$ perf stat ./perf-test-div 10

 Performance counter stats for './perf-test-div 10':

    41,768,314,396      cycles                    #    3.430 GHz                       
     4,014,826,989      instructions              #    0.10  insns per cycle        
     1,002,957,543      branches                  #   82.369 M/sec                  

      12.177372636 seconds time elapsed

Hier haben wir ~ 42e9 Zyklen benötigt, um 1e9 Iterationen durchzuführen, und wir hatten ~ 14.800.000 zusätzliche Anweisungen. Das ist vergleichbar mit nur ~ 400.000 zusätzlichen Anweisungen für die gleichen 1e9-Schleifen mitnop. Wenn wir mit dem @ vergleichnop Schleife, die ungefähr die gleiche Anzahl von @ nimcycles (40e9-Iterationen) sehen wir fast genau die gleiche Anzahl zusätzlicher Anweisungen:

~/dev/perf-test$ perf stat ./perf-test-nop 41

 Performance counter stats for './perf-test-nop 41':

    41,145,332,629      cycles                    #    3.425 
   164,013,912,324      instructions              #    3.99  insns per cycle        
    41,002,424,948      branches                  # 3412.968 M/sec                  

      12.013355313 seconds time elapsed

Was ist los mit dieser mysteriösen Arbeit im Kernel?

1 Hier verwende ich die Begriffe "Zeit" und "Zyklen" mehr oder weniger austauschbar. Die CPU läuft während dieser Tests auf Hochtouren, daher modulieren einige turboaufladungsbedingte thermische Effekte die Zyklen direkt proportional zur Zeit.

Antworten auf die Frage(2)

Ihre Antwort auf die Frage