Ich habe 2009 zum ersten Mal festgestellt, dass GCC (zumindest bei meinen Projekten und auf meinen Maschinen) die Tendenz hat, merklich schnelleren Code zu generieren, wenn ich für Größe ( -Os
) anstelle von Geschwindigkeit ( -O2
oder -O3
) optimiere , und ich habe mich seitdem gefragt, warum.
Ich habe es geschafft, (ziemlich albernen) Code zu erstellen, der dieses überraschende Verhalten zeigt und klein genug ist, um hier veröffentlicht zu werden.
const int LOOP_BOUND = 200000000;
__attribute__((noinline))
static int add(const int& x, const int& y) {
return x + y;
}
__attribute__((noinline))
static int work(int xval, int yval) {
int sum(0);
for (int i=0; i<LOOP_BOUND; ++i) {
int x(xval+sum);
int y(yval+sum);
int z = add(x, y);
sum += z;
}
return sum;
}
int main(int , char* argv[]) {
int result = work(*argv[1], *argv[2]);
return result;
}
Wenn ich es mit kompiliere -Os
, dauert es 0,38 s, um dieses Programm auszuführen, und 0,44 s, wenn es mit -O2
oder kompiliert wird -O3
. Diese Zeiten werden konsistent und praktisch rauschfrei erhalten (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).
(Update: Ich habe den gesamten Assemblycode auf GitHub verschoben : Sie haben den Beitrag aufgebläht und den Fragen anscheinend nur einen sehr geringen Wert verliehen, da die fno-align-*
Flags den gleichen Effekt haben.)
Hier ist die generierte Baugruppe mit -Os
und -O2
.
Leider ist mein Verständnis der Baugruppe sehr begrenzt, so dass ich keine Ahnung habe, ob das, was ich als Nächstes getan habe, richtig war: Ich habe die Baugruppe für -O2
alle Baugruppen mit -Os
Ausnahme der .p2align
Linien in die Baugruppe eingefügt . Dieser Code läuft immer noch in 0,38 Sekunden und der einzige Unterschied ist das .p2align
Zeug.
Wenn ich richtig denke, sind dies Auffüllungen für die Stapelausrichtung. Laut Warum funktioniert das GCC-Pad mit NOPs? Es wird in der Hoffnung gemacht, dass der Code schneller ausgeführt wird, aber anscheinend schlug diese Optimierung in meinem Fall fehl.
Ist es die Polsterung, die in diesem Fall die Schuld trägt? Warum und wie?
Das Rauschen, das es macht, macht zeitliche Mikrooptimierungen unmöglich.
Wie kann ich sicherstellen, dass solche versehentlichen glücklichen / unglücklichen Ausrichtungen nicht stören, wenn ich Mikrooptimierungen (unabhängig von der Stapelausrichtung) für C- oder C ++ - Quellcode durchführe?
AKTUALISIEREN:
Nach der Antwort von Pascal Cuoq bastelte ich ein wenig an den Ausrichtungen. Durch die Übergabe -O2 -fno-align-functions -fno-align-loops
an gcc werden alle .p2align
aus der Assembly entfernt und die generierte ausführbare Datei wird in 0,38 Sekunden ausgeführt. Laut gcc-Dokumentation :
-Os aktiviert alle -O2-Optimierungen [aber] -Os deaktiviert die folgenden Optimierungsflags:
-falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays
Es scheint also so ziemlich ein (falsches) Ausrichtungsproblem zu sein.
Ich bin immer noch skeptisch, -march=native
wie in Marat Dukhans Antwort vorgeschlagen . Ich bin nicht davon überzeugt, dass es nicht nur dieses (falsche) Ausrichtungsproblem stört. es hat absolut keine Auswirkung auf meine Maschine. (Trotzdem habe ich seine Antwort positiv bewertet.)
UPDATE 2:
Wir können -Os
aus dem Bild herausnehmen. Die folgenden Zeiten werden durch Kompilieren mit erhalten
-O2 -fno-omit-frame-pointer
0,37 s-O2 -fno-align-functions -fno-align-loops
0,37 s-S -O2
dann manuelles Verschieben der Baugruppeadd()
nachwork()
0,37 s-O2
0,44 s
Mir scheint, dass die Entfernung add()
von der Anrufstelle sehr wichtig ist. Ich habe es versucht perf
, aber die Ausgabe von perf stat
und perf report
macht für mich sehr wenig Sinn. Ich konnte jedoch nur ein konsistentes Ergebnis erzielen:
-O2
::
602,312,864 stalled-cycles-frontend # 0.00% frontend cycles idle
3,318 cache-misses
0.432703993 seconds time elapsed
[...]
81.23% a.out a.out [.] work(int, int)
18.50% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
100.00 ¦ lea (%rdi,%rsi,1),%eax
¦ }
¦ ? retq
[...]
¦ int z = add(x, y);
1.93 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
79.79 ¦ add %eax,%ebx
Für fno-align-*
:
604,072,552 stalled-cycles-frontend # 0.00% frontend cycles idle
9,508 cache-misses
0.375681928 seconds time elapsed
[...]
82.58% a.out a.out [.] work(int, int)
16.83% a.out a.out [.] add(int const&, int const&) [clone .isra.0]
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ return x + y;
51.59 ¦ lea (%rdi,%rsi,1),%eax
¦ }
[...]
¦ __attribute__((noinline))
¦ static int work(int xval, int yval) {
¦ int sum(0);
¦ for (int i=0; i<LOOP_BOUND; ++i) {
¦ int x(xval+sum);
8.20 ¦ lea 0x0(%r13,%rbx,1),%edi
¦ int y(yval+sum);
¦ int z = add(x, y);
35.34 ¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
39.48 ¦ add %eax,%ebx
¦ }
Für -fno-omit-frame-pointer
:
404,625,639 stalled-cycles-frontend # 0.00% frontend cycles idle
10,514 cache-misses
0.375445137 seconds time elapsed
[...]
75.35% a.out a.out [.] add(int const&, int const&) [clone .isra.0] ¦
24.46% a.out a.out [.] work(int, int)
[...]
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
18.67 ¦ push %rbp
¦ return x + y;
18.49 ¦ lea (%rdi,%rsi,1),%eax
¦ const int LOOP_BOUND = 200000000;
¦
¦ __attribute__((noinline))
¦ static int add(const int& x, const int& y) {
¦ mov %rsp,%rbp
¦ return x + y;
¦ }
12.71 ¦ pop %rbp
¦ ? retq
[...]
¦ int z = add(x, y);
¦ ? callq add(int const&, int const&) [clone .isra.0]
¦ sum += z;
29.83 ¦ add %eax,%ebx
Es sieht so aus, als würden wir add()
im langsamen Fall auf dem Anruf stehen bleiben .
Ich habe untersucht alles , die perf -e
auf meiner Maschine ausspucken kann; nicht nur die Statistiken, die oben angegeben sind.
Für dieselbe ausführbare Datei stalled-cycles-frontend
zeigt die lineare Korrelation mit der Ausführungszeit; Ich habe nichts anderes bemerkt, was so klar korrelieren würde. (Das Vergleichen stalled-cycles-frontend
verschiedener ausführbarer Dateien macht für mich keinen Sinn.)
Ich habe die Cache-Fehler als ersten Kommentar eingefügt. Ich habe alle Cache-Fehler untersucht, die auf meinem Computer gemessen werden können perf
, nicht nur die oben angegebenen. Die Cache-Fehler sind sehr, sehr verrauscht und zeigen wenig bis gar keine Korrelation mit den Ausführungszeiten.