Perf Überzählen einfacher CPU-gebundener Schleife: mysteriöser Kernel funktioniert?

8

Ich habe Linux perf schon seit einiger Zeit verwendet, um Anwendungsprofile zu erstellen. Normalerweise ist die profilierte Anwendung ziemlich komplex, sodass man die gemeldeten Zählerwerte einfach auf den Nennwert nimmt, solange keine grobe Diskrepanz zu dem besteht, was man aufgrund der ersten Prinzipien erwarten könnte.

In letzter Zeit habe ich jedoch einige triviale 64-Bit-Assemblierungsprogramme erstellt - so trivial, dass man fast genau den erwarteten Wert verschiedener Zähler berechnen kann, und es scheint, dass perf stat überzählig ist.

Nehmen Sie die folgende Schleife zum Beispiel:

%Vor%

Dies wird einfach n mal wiederholen, wobei n der Anfangswert von rax ist. Jede Iteration der Schleife führt 4 Anweisungen aus, sodass Sie erwarten, dass 4 * n Anweisungen ausgeführt werden, plus ein kleiner fester Overhead für den Prozessstart und -ende und das kleine Code-Bit, das n vor dem Eintritt in die Schleife setzt.

Hier ist die (typische) perf stat Ausgabe für n = 1,000,000,000 :

%Vor%

Huh. Statt etwa 4.000.000.000 Instruktionen und 1.000.000.000 Verzweigungen sehen wir eine mysteriöse zusätzliche 410.032 Instruktionen und 71.277 Äste. Es gibt immer "extra" Anweisungen, aber der Betrag variiert ein wenig - nachfolgende Läufe zum Beispiel hatten 421K, 563K und 464K extra Anweisungen. Sie können dies selbst auf Ihrem System ausführen, indem Sie mein einfaches github-Projekt erstellen.

OK, also könnten Sie vermuten, dass diese paar hunderttausend zusätzlichen Anweisungen nur feste Kosten für die Einrichtung und den Abbau von Anwendungen sind (die Benutzerland-Konfiguration ist sehr klein , aber es könnte versteckte Sachen geben). Versuchen wir für n=10 billion then:

%Vor%

Jetzt gibt es ~ 4,9 Millionen zusätzliche Anweisungen, etwa 10 Mal mehr als zuvor, proportional zum 10-fachen Anstieg der Schleifenanzahl.

Sie können verschiedene Zähler testen - alle CPU-bezogenen zeigen ähnliche proportionale Zunahmen. Konzentrieren wir uns dann auf die Befehlszählung, um die Dinge einfach zu halten. Mit den Suffixen :u und :k zur Messung der Benutzer und Kernel werden die im Kernel Konto aufgetretenen Zählungen für beinahe angezeigt alle zusätzlichen Ereignisse:

%Vor%

Bingo. Von den 389.050 zusätzlichen Anweisungen waren 99.98% (388.958) im Kernel enthalten.

OK, aber wo bleibt uns das? Dies ist eine triviale CPU-gebundene Schleife. Es führt keine Systemaufrufe aus und greift nicht auf Speicher zu (der indirekt den Kernel über den Seitenfehlermechanismus aufrufen 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 bei null liegen, und in jedem Fall korreliert die Anzahl der zusätzlichen -Anweisungen nicht mit Läufen, bei denen mehr vorhanden ist dieser Ereignisse ist aufgetreten.

Die Anzahl der zusätzlichen Kernel-Anweisungen ist in der Tat sehr glatt mit der Anzahl der Schleifen. Hier ist ein Diagramm von (Milliarden von) Schleifeniterationen gegenüber Kernel-Anweisungen:

Sie können sehen, dass die Beziehung ziemlich perfekt linear ist - tatsächlich gibt es bis 15e9 Iterationen nur einen Ausreißer. Danach scheint es zwei getrennte Linien zu geben, die eine Art Quantisierung von allem, was die Überzeit verursacht, nahelegen. In jedem Fall werden für jede 1e9-Anweisung, die in der Hauptschleife ausgeführt wird, etwa 350K-Kernel-Befehle benötigt.

Abschließend bemerkte ich, dass die Anzahl der ausgeführten Kernel-Anweisungen eher proportional zu Laufzeit 1 (oder CPU-Zeit) als den ausgeführten Anweisungen ist. Um das zu testen, verwende ich ein ähnliches Programm , aber mit einem von Die Anweisungen nop werden durch idiv ersetzt, die eine Latenz von etwa 40 Zyklen haben (einige uninteressante Zeilen wurden entfernt):

%Vor%

Hier haben wir ~ 429 Zyklen genommen, um 1e9 Iterationen zu vervollständigen, und wir hatten ~ 14.800.000 zusätzliche Anweisungen. Dies entspricht nur ~ 400.000 zusätzlichen Anweisungen für die gleichen 1e9-Schleifen mit nop . Wenn wir mit der nop -Schleife vergleichen, die ungefähr die gleiche Anzahl von cycles (40e9 Iterationen) annimmt, sehen wir fast genau die gleiche Anzahl zusätzlicher Anweisungen:

%Vor%

Was ist los mit dieser geheimnisvollen Arbeit im Kernel?

1 Hier verwende ich die Begriffe "Zeit" und "Zyklen" mehr oder weniger austauschbar hier. Die CPU läuft während dieser Tests schnell aus, also modulo einige Turbo-Boost bezogene thermische Effekte, Zyklen sind direkt proportional zur Zeit.

    
BeeOnRope 05.10.2016, 02:11
quelle

1 Antwort

7

TL; DR

Die Antwort ist ziemlich einfach. Ihre Zähler sind so eingestellt, dass sie sowohl im als auch OS -Modus zählen, und Ihre Messungen werden in regelmäßigen Abständen durch Linux-Zeitscheiben gestört Scheduler, der mehrmals pro Sekunde tickt .

Zufällig für Sie, bei der Untersuchung eines nicht verwandten Problems mit @PeterCordes vor 5 Tagen, ich hat eine bereinigte Version meiner eigenen Leistungscounter-Zugriffssoftware veröffentlicht , libpfc .

libpfc

libpfc ist eine sehr low-level Bibliothek und Linux Loadable Kernel Module, die ich selbst mit nur den vollständigen Intel Software-Entwicklerhandbuch als Referenz. Die Leistungszähleinrichtung ist in Band 3, Kapitel 18-19 des SDM dokumentiert. Es wird konfiguriert, indem spezifische Werte für bestimmte MSRs (modellspezifische Register) in bestimmten x86-Prozessoren geschrieben werden.

Es gibt zwei Arten von Zählern, die auf meinem Intel Haswell-Prozessor konfiguriert werden können:

  • Fixed-Function-Counter . Sie beschränken sich auf das Zählen einer bestimmten Art von Ereignis. Sie können aktiviert oder deaktiviert werden, aber was sie verfolgen, kann nicht geändert werden. Auf dem 2,4 GHz Haswell i7-4700MQ gibt es 3:

    • Anweisungen ausgestellt : Was es auf der Dose sagt.
    • Unhalted Core Cycles : Die Anzahl der tatsächlich aufgetretenen Clock-Ticks. Wenn die Frequenz des Prozessors nach oben oder nach unten skaliert wird, beginnt dieser Zähler schneller bzw. langsamer zu zählen.
    • Unhalted Reference Cycles : Die Anzahl der Takt-Ticks, die bei konstanter Frequenz ohne Auswirkung auf die dynamische Frequenzskalierung ticken. Auf meinem 2,4 GHz-Prozessor tickt es genau bei 2,4 GHz. Daher gibt Unhalted Reference / 2.4e9 ein Sub-Nanosekunden-Genauigkeits-Timing und Unhalted Core / Unhalted Reference den durchschnittlichen Speedup-Faktor, den Sie durch Turbo Boost erhalten haben.
  • Allzweckzähler . Diese können im Allgemeinen so konfiguriert werden, dass sie jedes Ereignis (mit nur wenigen Einschränkungen) verfolgen, das im SDM für Ihren bestimmten Prozessor aufgeführt ist. Für Haswell sind sie derzeit in der Volume 3, § 19.4 der SDMs aufgeführt und mein Repository enthält eine Demo, pfcdemo , die auf eine große Teilmenge von ihnen zugreift. Sie sind bei pfcdemo.c:169 aufgelistet. Wenn HyperThreading auf meinem Haswell-Prozessor aktiviert ist, hat jeder Kern 4 solcher Zähler.

Die Zähler richtig konfigurieren

Um die Zähler zu konfigurieren, übernahm ich die Last, jede MSR selbst in meinem LKM zu programmieren, pfc.ko , dessen Quellcode in meinem Repository enthalten ist .

Das Programmieren von MSRs muss extrem vorsichtig erfolgen, sonst wird der Prozessor Sie mit einer Kernel Panic bestrafen. Aus diesem Grund habe ich mich mit jedem einzelnen Bit von fünf verschiedenen Arten von MSRs vertraut gemacht, zusätzlich zu den Allzweck- und Festfunktionszählern selbst. Meine Notizen zu diesen Registern sind bei pfckmod.c:750 und werden hier wiedergegeben:

%Vor%

Beachten Sie insbesondere IA32_PERFEVTSELx , Bits 16 (Benutzermodus) und 17 (OS-Modus) und IA32_FIXED_CTR_CTRL , Bits IA32_FIXED_CTRx enable . IA32_PERFEVTSELx konfiguriert den allgemeinen Zähler x , während jede Gruppe von 4 Bits beginnend mit dem Bit 4*x , das vom LSB in IA32_FIXED_CTR_CTRL zählt, den Zähler für feste Funktionen x konfiguriert.

Wenn in der MSR IA32_PERFEVTSELx das Betriebssystembit gelöscht wird, während das Benutzerbit gesetzt ist, akkumuliert der Zähler nur Ereignisse im Benutzermodus und schließt Kernelmodusereignisse aus. In der MSR IA32_FIXED_CTRL_CTRL enthält jede Gruppe von 4 Bits ein 2-bit enable -Feld, das, wenn es auf 2 ( 0b10 ) gesetzt ist, das Zählen von Ereignissen im Benutzermodus ermöglicht, aber nicht im Kernel-Modus.

Mein LKM erzwingt die Zählung im Nur-Nutzer-Modus für Zähler mit fester Funktion und für allgemeine Zwecke unter pfckmod.c:296 und pfckmod.c:330 .

Benutzer-Speicherplatz

Im Benutzerbereich konfiguriert der Benutzer die Zähler (Beispiel des Prozesses, beginnend bei pfcdemo.c:98 ), dann wird der Code mit den Makros PFCSTART() und PFCEND() getaktet. Dies sind sehr spezifische Codefolgen, aber sie haben Kosten und erzeugen somit ein verzerrtes Ergebnis, das die Anzahl der Ereignisse von den Zeitgebern übersteigt.Daher müssen Sie auch pfcRemoveBias() , die Anzahl PFCSTART()/PFCEND() aufrufen, wenn sie 0 Anweisungen umgeben, und die Abweichung von der akkumulierten Zählung entfernen.

Ihr Code, unter libpfc

Ich habe Ihren Code genommen und in pfcdemo.c:130 gestellt, z das es jetzt gelesen hat

%Vor%

. Ich habe Folgendes:

%Vor%

Kein Aufwand mehr! Aus den Zählern mit festen Funktionen (z. B. dem letzten Satz von Ausdrucken) ist ersichtlich, dass der IPC 4000000082 / 1008514841 ist, also etwa 4 IPC, von 757761792 / 2.4e9 , dass der Code 0,31573408 Sekunden beanspruchte, und von 1008514841 / 757761792 = 1,330912763941521 der Kern war Turbo Boosting auf 133% von 2,4 GHz oder 3,2 GHz.

    
Iwillnotexist Idonotexist 08.10.2016, 18:58
quelle