Warum beginnt diese Verzögerungsschleife nach mehreren Iterationen ohne Schlaf schneller zu laufen?

73

Erwägen:

#include <time.h>
#include <unistd.h>
#include <iostream>
using namespace std;

const int times = 1000;
const int N = 100000;

void run() {
  for (int j = 0; j < N; j++) {
  }
}

int main() {
  clock_t main_start = clock();
  for (int i = 0; i < times; i++) {
    clock_t start = clock();
    run();
    cout << "cost: " << (clock() - start) / 1000.0 << " ms." << endl;
    //usleep(1000);
  }
  cout << "total cost: " << (clock() - main_start) / 1000.0 << " ms." << endl;
}

Hier ist der Beispielcode. In den ersten 26 Iterationen der Zeitschleife runkostet die Funktion ungefähr 0,4 ms, aber dann reduzieren sich die Kosten auf 0,2 ms.

Wenn das nicht usleepkommentiert ist, dauert die Verzögerungsschleife für alle Läufe 0,4 ms und beschleunigt nie. Warum?

Der Code wird mit kompiliert g++ -O0(keine Optimierung), sodass die Verzögerungsschleife nicht entfernt wird. Es läuft auf Intel (R) Core (TM) i3-3220- CPU mit 3,30 GHz und Ubuntu 14.04.1 LTS (Trusty Tahr) mit 3.13.0-32-Generika .

phyxnj
quelle
Sie sollten wahrscheinlich das Ergebnis überprüfen, usleep()da es möglicherweise unterbrochen wird oder nichts unternimmt, da Ihr Parameter nicht gültig ist. Dies würde jegliches Timing unzuverlässig machen.
John3136
@ John3136: Der Schlaf befindet sich außerhalb des Zeitfensters. Er plant eine Besetztschleife entweder Rücken an Rücken oder durch 1 ms Schlaf getrennt.
Peter Cordes
1
Für Benchmarking-Zwecke sollten Sie mindestens mit gcc -O2oder kompilieren (da Ihr Code C ++ ist) g++ -O2.
Basile Starynkevitch
1
Wenn Sie 1000 Mikrosekunden schlafen, würde ich erwarten, dass die Schleife mindestens 1 Millisekunde dauert. Wie messen Sie 0,4 ms?
Adrian McCarthy
2
@AdrianMcCarthy: das usleepist außerhalb des Zeitfensters
Peter Cordes

Antworten:

125

Nach 26 Iterationen, Rampen Linux die CPU auf die maximale Taktfrequenz bis da Ihr Prozess seine volle verwendet Zeitscheibe ein paar Mal in Folge.

Wenn Sie anstelle der Wanduhrzeit Leistungsindikatoren verwenden, werden Sie feststellen, dass die Kerntaktzyklen pro Verzögerungsschleife konstant bleiben, was bestätigt, dass dies nur ein Effekt von DVFS ist (das alle modernen CPUs verwenden, um mit mehr Energie zu arbeiten). meistens effiziente Frequenz und Spannung).

Wenn Sie auf einem Skylake mit Kernel-Unterstützung für den neuen Energieverwaltungsmodus (bei dem die Hardware die volle Kontrolle über die Taktrate übernimmt) getestet haben , würde der Hochlauf viel schneller erfolgen.

Wenn Sie es für eine Weile auf einer Intel-CPU mit Turbo laufen lassen , wird sich die Zeit pro Iteration wahrscheinlich wieder leicht erhöhen, sobald die Taktrate bei thermischen Grenzwerten wieder auf die maximal anhaltende Frequenz reduziert werden muss. (Weitere Informationen zu Turbo, mit dem die CPU schneller ausgeführt werden kann als bei Hochleistungs-Workloads, finden Sie unter Warum kann meine CPU die Spitzenleistung in HPC nicht aufrechterhalten ?)


Die Einführung von ausleep verhindert, dass der CPU-Frequenzregler von Linux die Taktrate erhöht, da der Prozess selbst bei minimaler Frequenz keine 100% ige Last erzeugt. (Das heißt, die Heuristik des Kernels entscheidet, dass die CPU schnell genug für die Arbeitslast läuft, die darauf ausgeführt wird.)



Kommentare zu anderen Theorien :

Betreff: Davids Theorie, dass ein möglicher Kontextwechsel usleepCaches verschmutzen könnte : Das ist im Allgemeinen keine schlechte Idee, aber es hilft nicht, diesen Code zu erklären.

Die Cache / TLB-Verschmutzung ist für dieses Experiment überhaupt nicht wichtig . Im Timing-Fenster befindet sich im Grunde nichts anderes als das Ende des Stapels, das den Speicher berührt. Die meiste Zeit wird in einer winzigen Schleife (1 Zeile Befehls-Cache) verbracht, die nur einen intStapelspeicher berührt . Jede mögliche Cache-Verschmutzung während usleepist nur ein winziger Bruchteil der Zeit für diesen Code (realer Code wird anders sein)!

Im Detail für x86:

Der Aufruf an sich clock()selbst kann einen Cache-Miss verursachen, aber ein Code-Fetch-Cache-Miss verzögert die Startzeitmessung, anstatt Teil dessen zu sein, was gemessen wird. Der zweite Aufruf von clock()wird fast nie verzögert, da er im Cache noch heiß sein sollte.

Die runFunktion befindet sich möglicherweise in einer anderen Cache-Zeile als main(da gcc mainals "kalt" markiert ist , wird sie weniger optimiert und zusammen mit anderen kalten Funktionen / Daten platziert). Wir können ein oder zwei Anweisungs-Cache-Fehler erwarten . Sie befinden sich jedoch wahrscheinlich immer noch auf derselben 4k-Seite, sodass mainder potenzielle TLB-Fehler ausgelöst wurde, bevor der zeitgesteuerte Bereich des Programms eingegeben wurde.

gcc -O0 kompiliert den OP-Code wie folgt (Godbolt Compiler Explorer) : Der Schleifenzähler bleibt im Speicher des Stapels.

Die leere Schleife hält den Schleifenzähler im Stapelspeicher, sodass auf einer typischen Intel x86-CPU die Schleife dank der Speicherweiterleitungslatenz, die Teil addeines Speicherziels ist (Lesen), mit einer Iteration pro ~ 6 Zyklen auf der IvyBridge-CPU des OP ausgeführt wird -modify-write). 100k iterations * 6 cycles/iterationbeträgt 600.000 Zyklen, was den Beitrag von höchstens ein paar Cache-Fehlern dominiert (jeweils ~ 200 Zyklen für Code-Abruf-Fehler, die verhindern, dass weitere Anweisungen ausgegeben werden, bis sie behoben sind).

Die Ausführung außerhalb der Reihenfolge und die Weiterleitung von Speichern sollten den potenziellen Cache-Fehler beim Zugriff auf den Stapel (als Teil der callAnweisung) größtenteils verbergen .

Selbst wenn der Schleifenzähler in einem Register gehalten wurde, sind 100.000 Zyklen eine Menge.

Peter Cordes
quelle
Ich erhöhe den Wert von Num das 100-fache und benutze den cpufreq-infoBefehl. Ich habe festgestellt, dass die CPU immer noch mit der Mindestfrequenz arbeitet, wenn der Code ausgeführt wird.
Phyxnj
@phyxnj: mit usleepnicht kommentiert? Es läuft für mich mit N = 10000000 hoch. (Ich benutze, grep MHz /proc/cpuinfoda ich nie dazu gekommen bin, cpufreq-utils auf diesem Computer zu installieren). Eigentlich habe ich gerade herausgefunden, cpupower frequency-infowas zeigt, was cpufreq-info für einen Kern getan hat.
Peter Cordes
@phyxnj: Bist du sicher, dass du alle Kerne betrachtest und nicht nur einen Kern? cpupowerscheint standardmäßig nur Kern 0 zu sein.
Peter Cordes
grep MHz /proc/cpuinfozeigt tatsächlich einen Anstieg der CPU-Frequenz. cpufreq-infoVielleicht überwachen Sie einen zufälligen Kern der CPU. Ich denke, Sie haben Recht, vielleicht ist dies die Ursache des Problems.
Phyxnj
1
@phyxnj: Es ist nicht zufällig und die Kernnummer wird in der Ausgabe ausgedruckt. zB thinkwiki.org/wiki/How_to_use_cpufrequtils . Es wird mit ziemlicher Sicherheit standardmäßig nur Kern 0 verwendet. Das einzige, was unvorhersehbar ist, ist, auf welchem ​​Kern Ihr Prozess ausgeführt wird.
Peter Cordes
3

Ein Aufruf von usleepkann zu einem Kontextwechsel führen oder auch nicht. Wenn dies der Fall ist, dauert es länger als wenn dies nicht der Fall ist.

David Schwartz
quelle
1
usleepgibt die CPU freiwillig frei, daher sollte es sicher einen Kontextwechsel geben (auch wenn das System inaktiv ist), nicht wahr?
Rakib_
1
@rakib Nicht, wenn es nichts gibt, zu dem der Kontext gewechselt werden kann, oder wenn das Zeitintervall zu kurz ist. Wenn Sie von weniger als 10 ms sprechen, entscheidet sich das Betriebssystem möglicherweise dafür, keinen Kontextwechsel durchzuführen.
David Schwartz
@rakib: Es gibt einen Wechsel in den Kernel-Modus und sicher zurück. Möglicherweise wird nicht zu einem anderen Prozess usleepgewechselt , bevor der aufgerufene Prozess fortgesetzt wird. Daher kann die Verschmutzung von Caches / TLBs / Verzweigungsprädiktoren minimal sein.
Peter Cordes
2
@rakib Dann findet er scheduleheraus, wie lange es bis zum nächsten Schritt dauert , und entscheidet dann, wie er warten soll, möglicherweise plant er etwas anderes, möglicherweise mithilfe eines Hardware-Timers, möglicherweise nicht.
David Schwartz
1
@rakib: Wenn auf der CPU nichts Wesentliches ausgeführt wird, bevor sie zum Aufrufer von zurückkehrt usleep, würden einige Leute sagen, dass es keinen Kontextwechsel gab, selbst wenn die Hardware (mit einer hltAnweisung) für kurze Zeit in den Ruhezustand ging . In diesem Fall gibt es definitiv eine minimale Cache- / TLB-Verschmutzung und IIRC keine TLB-Ungültigkeit. (Ich vergesse genau, wie die Seitentabellen für den Kernelmodus funktionieren, aber ich denke nicht, dass der gesamte TLB bei jedem Systemaufruf weggeblasen werden muss).
Peter Cordes