Ich habe zum ersten Mal im Jahr 2009 festgestellt, dass GCC (zumindest bei meinen Projekten und auf meinen Computern) die Tendenz hat, merklich schnelleren Code zu generieren, wenn ich auf Größe optimiere ( -Os
) Statt Geschwindigkeit (-O2
Oder -O3
), Und ich habe mich seitdem gewundert, 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 -Os
Kompiliere, dauert es 0,38 s, um dieses Programm auszuführen, und 0,44 s, wenn es mit -O2
Oder -O3
Kompiliert wird. Diese Zeiten werden konsistent und praktisch geräuschlos erhalten (gcc 4.7.2, x86_64 GNU/Linux, Intel Core i5-3320M).
(Update: Ich habe den gesamten Assembly-Code nach GitHub verschoben : Sie haben den Beitrag aufgebläht und den Fragen anscheinend nur sehr geringen Wert verliehen, da der fno-align-*
Flags haben den gleichen Effekt.)
Hier ist die generierte Assembly mit -Os
und -O2
.
Leider ist mein Verständnis von Assembly sehr begrenzt, sodass ich keine Ahnung habe, ob das, was ich als Nächstes getan habe, richtig war: Ich habe die Assembly für -O2
Gepackt und alle ihre Unterschiede in der Assembly für -Os
= Zusammengeführt ausgenommen Die Zeilen .p2align
ergeben hier . Dieser Code läuft immer noch in 0.38s und der einzige Unterschied ist das .p2align
Zeug.
Wenn ich richtig schätze, sind dies Auffüllungen für die Stapelausrichtung. Laut Warum funktioniert das GCC-Pad mit NOPs? wird in der Hoffnung gearbeitet, dass der Code schneller läuft, aber anscheinend schlug diese Optimierung in meinem Fall fehl.
Ist es die Polsterung, die in diesem Fall die Ursache ist? Warum und wie?
Das Geräusch, das es macht, macht zeitliche Mikrooptimierungen unmöglich.
Wie kann ich sicherstellen, dass solche zufälligen glücklichen/unglücklichen Ausrichtungen nicht stören, wenn ich Mikrooptimierungen (unabhängig von der Stapelausrichtung) in C- oder C++ - Quellcode vornehme?
UPDATE:
Nach Pascal Cuoqs Antwort habe ich ein wenig an den Ausrichtungen herumgebastelt. Wenn Sie -O2 -fno-align-functions -fno-align-loops
An gcc übergeben, werden alle .p2align
Aus der Assembly entfernt und die generierte ausführbare Datei wird in 0,38 Sekunden ausgeführt. Laut der gcc-Dokumentation :
-Os aktiviert alle -O2-Optimierungen [aber] -Os deaktiviert die folgenden Optimierungsflags:
-falign-functions -falign-jumps -falign-loops <br/> -falign-labels -freorder-blocks -freorder-blocks-and-partition <br/> -fprefetch-loop-arrays <br/>
Es scheint also so ziemlich ein (falsches) Ausrichtungsproblem zu sein.
Ich bin immer noch skeptisch gegenüber -march=native
, Wie in Marat Dukhans Antwort vorgeschlagen. Ich bin nicht davon überzeugt, dass es nicht nur diese (falsche) Ausrichtung 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 entfernen. Die folgenden Zeiten ergeben sich durch Kompilieren mit
-O2 -fno-omit-frame-pointer
0,37s
-O2 -fno-align-functions -fno-align-loops
0,37s
-S -O2
Und dann manuelles Verschieben der Assembly von add()
nach work()
0.37s
-O2
0,44s
Es scheint mir, dass die Entfernung von add()
von der Anrufstelle sehr wichtig ist. Ich habe perf
ausprobiert, aber die Ausgabe von perf stat
Und perf report
Macht für mich wenig Sinn. Ich konnte jedoch nur ein einheitliches 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 den Aufruf von add()
im langsamen Fall blockieren.
Ich habe alles untersucht, dass perf -e
Auf meine Maschine ausspucken kann; nicht nur die Statistiken, die oben angegeben sind.
Für dieselbe ausführbare Datei zeigt stalled-cycles-frontend
Eine lineare Korrelation mit der Ausführungszeit. Mir ist nichts anderes aufgefallen, das so eindeutig korrelieren würde. (Ein Vergleich von stalled-cycles-frontend
Für verschiedene ausführbare Dateien ergibt für mich keinen Sinn.)
Ich habe die Cache-Misses als ersten Kommentar hinzugefügt. Ich habe alle Cache-Fehler untersucht, die auf meinem Computer mit perf
gemessen werden können, nicht nur die oben angegebenen. Die Cache-Misses sind sehr, sehr verrauscht und korrelieren kaum mit den Ausführungszeiten.
Mein Kollege hat mir geholfen, eine plausible Antwort auf meine Frage zu finden. Er bemerkte die Wichtigkeit der 256-Byte-Grenze. Er ist hier nicht registriert und hat mich ermutigt, die Antwort selbst zu posten (und all den Ruhm zu nehmen).
Kurze Antwort:
Ist es die Polsterung, die in diesem Fall die Ursache ist? Warum und wie?
Alles läuft auf die Ausrichtung hinaus. Ausrichtungen können sich erheblich auf die Leistung auswirken. Deshalb haben wir die -falign-*
- Flags in der erster Platz.
Ich habe einen (gefälschten?) Fehlerbericht an die gcc-Entwickler gesendet . Es stellt sich heraus, dass das Standardverhalten "Wir richten Schleifen standardmäßig auf 8 Byte aus, aber versuchen, es auf 16 Byte auszurichten, wenn wir nicht mehr als 10 Byte ausfüllen müssen." Anscheinend Diese Standardeinstellung ist in diesem speziellen Fall und auf meinem Computer nicht die beste Wahl. Clang 3.4 (trunk) mit -O3
Führt die entsprechende Ausrichtung durch und der generierte Code zeigt dieses seltsame Verhalten nicht.
Natürlich verschlechtert eine unangemessene Ausrichtung die Situation. Eine unnötige/fehlerhafte Ausrichtung verschlingt nur Bytes ohne Grund und erhöht möglicherweise die Cache-Fehler , usw.
Das Geräusch, das es macht, macht zeitliche Mikrooptimierungen unmöglich.
Wie kann ich sicherstellen, dass solche zufälligen glücklichen/unglücklichen Ausrichtungen nicht stören, wenn ich Mikrooptimierungen (unabhängig von der Stapelausrichtung) an C- oder C++ - Quellcodes vornehme?
Einfach, indem Sie gcc anweisen, die richtige Ausrichtung vorzunehmen:
g++ -O2 -falign-functions=16 -falign-loops=16
Lange Antwort:
Der Code wird langsamer ausgeführt, wenn:
ein XX
Byte-Rand schneidet add()
in die Mitte (XX
ist maschinenabhängig).
wenn der Aufruf von add()
über eine XX
Byte-Grenze springen muss und das Ziel nicht ausgerichtet ist.
if add()
ist nicht ausgerichtet.
wenn die Schleife nicht ausgerichtet ist.
Die ersten 2 sind auf den Codes und Ergebnissen gut sichtbar, die Marat Dukhan hat freundlicherweise geschrieben . In diesem Fall gcc-4.8.1 -Os
(Wird in 0,994 Sekunden ausgeführt):
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3
ein 256-Byte-Rand schneidet add()
genau in die Mitte und weder add()
noch die Schleife sind ausgerichtet. Überraschung, Überraschung, das ist der langsamste Fall!
Im Fall von gcc-4.7.3 -Os
(Wird in 0,822 Sekunden ausgeführt) schneidet die 256-Byte-Grenze nur in einen kalten Abschnitt (aber weder die Schleife noch add()
werden geschnitten):
00000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
[...]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>
Nichts ist ausgerichtet und der Aufruf von add()
muss über die 256-Byte-Grenze springen. Dieser Code ist der zweitlangsamste.
Falls gcc-4.6.4 -Os
(Wird in 0,709 Sekunden ausgeführt), obwohl nichts ausgerichtet ist, muss der Aufruf von add()
nicht über die 256-Byte-Grenze springen und das Ziel ist genau 32 Byte entfernt:
4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>
Dies ist die schnellste von allen dreien. Warum die 256-Byte-Grenze auf seiner Maschine besonders ist, überlasse ich ihm, um es herauszufinden. Ich habe keinen solchen Prozessor.
Jetzt bekomme ich auf meinem Computer diesen 256-Byte-Boundary-Effekt nicht. Nur die Funktion und die Schleifenausrichtung sind auf meiner Maschine aktiv. Wenn ich g++ -O2 -falign-functions=16 -falign-loops=16
Übergebe, ist alles wieder normal: Ich erhalte immer den schnellsten Fall und die Zeit reagiert nicht mehr auf das -fno-omit-frame-pointer
- Flag. Ich kann g++ -O2 -falign-functions=32 -falign-loops=32
Oder ein Vielfaches von 16 übergeben, der Code reagiert auch nicht darauf.
Ich habe 2009 zum ersten Mal bemerkt, dass gcc (zumindest in meinen Projekten und auf meinen Computern) dazu neigt, merklich schnelleren Code zu generieren, wenn ich die Größe (-Os) anstelle der Geschwindigkeit (-O2 oder -O3) optimiere und mich gefragt habe seitdem warum.
Eine wahrscheinliche Erklärung ist, dass ich Hotspots hatte, die für die Ausrichtung empfindlich waren, genau wie in diesem Beispiel. Durch das Durcheinander mit den Flags (Übergabe von -Os
Anstelle von -O2
) Wurden diese Hotspots auf glückliche Weise versehentlich ausgerichtet und der Code wurde schneller. Es hatte nichts mit der Größenoptimierung zu tun: Es war ein Zufall, dass die Hotspots besser ausgerichtet wurden. Von nun an werde ich die Auswirkungen von überprüfen Ausrichtung auf meine Projekte.
Oh, und noch eine Sache. Wie können solche Hotspots entstehen, wie im Beispiel gezeigt? Wie kann das Inlining einer so kleinen Funktion wie add()
fehlschlagen?
Bedenken Sie:
// add.cpp
int add(const int& x, const int& y) {
return x + y;
}
und in einer separaten Datei:
// main.cpp
int add(const int& x, const int& y);
const int LOOP_BOUND = 200000000;
__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;
}
und kompiliert als: g++ -O2 add.cpp main.cpp
.
gcc integriert add()
nicht!
Das ist alles, es ist so einfach, unbeabsichtigt Hotspots wie die im OP zu erstellen. Natürlich ist es zum Teil meine Schuld: gcc ist ein ausgezeichneter Compiler. Kompiliere obiges als: g++ -O2 -flto add.cpp main.cpp
, Das heißt Wenn ich die Verbindungszeit optimiere, läuft der Code in 0.19s!
(Inlining ist im OP künstlich deaktiviert, daher war der Code im OP 2x langsamer).
Standardmäßig optimieren Compiler für "durchschnittliche" Prozessoren. Da unterschiedliche Prozessoren unterschiedliche Befehlssequenzen bevorzugen, können durch -O2
Aktivierte Compileroptimierungen dem durchschnittlichen Prozessor zugute kommen, verringern jedoch die Leistung auf Ihrem bestimmten Prozessor (und dies gilt auch für -Os
). Wenn Sie dasselbe Beispiel auf verschiedenen Prozessoren ausprobieren, werden Sie feststellen, dass einige von -O2
Profitieren, während andere für -Os
- Optimierungen günstiger sind.
Hier sind die Ergebnisse für time ./test 0 0
Auf mehreren Prozessoren (Benutzerzeit angegeben):
Processor (System-on-Chip) Compiler Time (-O2) Time (-Os) Fastest
AMD Opteron 8350 gcc-4.8.1 0.704s 0.896s -O2
AMD FX-6300 gcc-4.8.1 0.392s 0.340s -Os
AMD E2-1800 gcc-4.7.2 0.740s 0.832s -O2
Intel Xeon E5405 gcc-4.8.1 0.603s 0.804s -O2
Intel Xeon E5-2603 gcc-4.4.7 1.121s 1.122s -
Intel Core i3-3217U gcc-4.6.4 0.709s 0.709s -
Intel Core i3-3217U gcc-4.7.3 0.708s 0.822s -O2
Intel Core i3-3217U gcc-4.8.1 0.708s 0.944s -O2
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s -Os
Intel Atom 330 gcc-4.8.1 2.003s 2.007s -O2
ARM 1176JZF-S (Broadcom BCM2835) gcc-4.6.3 3.470s 3.480s -O2
ARM Cortex-A8 (TI OMAP DM3730) gcc-4.6.3 2.727s 2.727s -
ARM Cortex-A9 (TI OMAP 4460) gcc-4.6.3 1.648s 1.648s -
ARM Cortex-A9 (Samsung Exynos 4412) gcc-4.6.3 1.250s 1.250s -
ARM Cortex-A15 (Samsung Exynos 5250) gcc-4.7.2 0.700s 0.700s -
Qualcomm Snapdragon APQ8060A gcc-4.8 1.53s 1.52s -Os
In einigen Fällen können Sie die Auswirkungen nachteiliger Optimierungen abmildern, indem Sie gcc
auffordern, für Ihren bestimmten Prozessor zu optimieren (mit den Optionen -mtune=native
Oder -march=native
):
Processor Compiler Time (-O2 -mtune=native) Time (-Os -mtune=native)
AMD FX-6300 gcc-4.8.1 0.340s 0.340s
AMD E2-1800 gcc-4.7.2 0.740s 0.832s
Intel Xeon E5405 gcc-4.8.1 0.603s 0.803s
Intel Core i7-4770K gcc-4.8.1 0.296s 0.288s
Update: Auf dem Ivy Bridge-basierten Core i3 erzeugen drei Versionen von gcc
(4.6.4
, 4.7.3
Und 4.8.1
) Binärdateien mit erheblich unterschiedlicher Leistung Assembler-Code weist nur geringfügige Abweichungen auf. Bisher habe ich keine Erklärung für diese Tatsache.
Assemblierung von gcc-4.6.4 -Os
(Wird in 0,709 Sekunden ausgeführt):
00000000004004d2 <_ZL3addRKiS0_.isra.0>:
4004d2: 8d 04 37 lea eax,[rdi+rsi*1]
4004d5: c3 ret
00000000004004d6 <_ZL4workii>:
4004d6: 41 55 Push r13
4004d8: 41 89 fd mov r13d,edi
4004db: 41 54 Push r12
4004dd: 41 89 f4 mov r12d,esi
4004e0: 55 Push rbp
4004e1: bd 00 c2 eb 0b mov ebp,0xbebc200
4004e6: 53 Push rbx
4004e7: 31 db xor ebx,ebx
4004e9: 41 8d 34 1c lea esi,[r12+rbx*1]
4004ed: 41 8d 7c 1d 00 lea edi,[r13+rbx*1+0x0]
4004f2: e8 db ff ff ff call 4004d2 <_ZL3addRKiS0_.isra.0>
4004f7: 01 c3 add ebx,eax
4004f9: ff cd dec ebp
4004fb: 75 ec jne 4004e9 <_ZL4workii+0x13>
4004fd: 89 d8 mov eax,ebx
4004ff: 5b pop rbx
400500: 5d pop rbp
400501: 41 5c pop r12
400503: 41 5d pop r13
400505: c3 ret
Assemblierung von gcc-4.7.3 -Os
(Wird in 0,822 Sekunden ausgeführt):
00000000004004fa <_ZL3addRKiS0_.isra.0>:
4004fa: 8d 04 37 lea eax,[rdi+rsi*1]
4004fd: c3 ret
00000000004004fe <_ZL4workii>:
4004fe: 41 55 Push r13
400500: 41 89 f5 mov r13d,esi
400503: 41 54 Push r12
400505: 41 89 fc mov r12d,edi
400508: 55 Push rbp
400509: bd 00 c2 eb 0b mov ebp,0xbebc200
40050e: 53 Push rbx
40050f: 31 db xor ebx,ebx
400511: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400516: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051a: e8 db ff ff ff call 4004fa <_ZL3addRKiS0_.isra.0>
40051f: 01 c3 add ebx,eax
400521: ff cd dec ebp
400523: 75 ec jne 400511 <_ZL4workii+0x13>
400525: 89 d8 mov eax,ebx
400527: 5b pop rbx
400528: 5d pop rbp
400529: 41 5c pop r12
40052b: 41 5d pop r13
40052d: c3 ret
Assemblierung von gcc-4.8.1 -Os
(Wird in 0.994 Sekunden ausgeführt):
00000000004004fd <_ZL3addRKiS0_.isra.0>:
4004fd: 8d 04 37 lea eax,[rdi+rsi*1]
400500: c3 ret
0000000000400501 <_ZL4workii>:
400501: 41 55 Push r13
400503: 41 89 f5 mov r13d,esi
400506: 41 54 Push r12
400508: 41 89 fc mov r12d,edi
40050b: 55 Push rbp
40050c: bd 00 c2 eb 0b mov ebp,0xbebc200
400511: 53 Push rbx
400512: 31 db xor ebx,ebx
400514: 41 8d 74 1d 00 lea esi,[r13+rbx*1+0x0]
400519: 41 8d 3c 1c lea edi,[r12+rbx*1]
40051d: e8 db ff ff ff call 4004fd <_ZL3addRKiS0_.isra.0>
400522: 01 c3 add ebx,eax
400524: ff cd dec ebp
400526: 75 ec jne 400514 <_ZL4workii+0x13>
400528: 89 d8 mov eax,ebx
40052a: 5b pop rbx
40052b: 5d pop rbp
40052c: 41 5c pop r12
40052e: 41 5d pop r13
400530: c3 ret
Ich füge dieses Post-Accept hinzu, um darauf hinzuweisen, dass die Auswirkungen der Ausrichtung auf die Gesamtleistung von Programmen - einschließlich großer Programme - untersucht wurden. Zum Beispiel zeigt dieser Artikel (und ich glaube, eine Version davon erschien auch in CACM), wie Änderungen der Verbindungsreihenfolge und der Größe der Betriebssystemumgebung allein ausreichten, um die Leistung signifikant zu verbessern. Sie führen dies auf die Ausrichtung von "Hot Loops" zurück.
Dieses Papier mit dem Titel "Falsche Daten produzieren, ohne etwas offensichtlich Falsches zu tun!" gibt an, dass versehentliche experimentelle Abweichungen aufgrund von nahezu unkontrollierbaren Unterschieden in Programmlaufumgebungen wahrscheinlich dazu führen, dass viele Benchmark-Ergebnisse bedeutungslos werden.
Ich glaube, Sie begegnen bei derselben Beobachtung einem anderen Blickwinkel.
Für leistungskritischen Code ist dies ein ziemlich gutes Argument für Systeme, die die Umgebung zur Installations- oder Laufzeit bewerten und aus unterschiedlich optimierten Versionen von Schlüsselroutinen die lokal beste auswählen.
Ich denke, dass Sie das gleiche Ergebnis erzielen können, wie Sie es getan haben:
Ich habe die Assembly für -O2 genommen und alle ihre Unterschiede mit Ausnahme der .p2align-Zeilen in der Assembly für -Os zusammengeführt:
... mit -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1
. Seit 15 Jahren habe ich mit diesen Optionen alles zusammengestellt, was schneller als normal -O2
War, wenn ich mir die Mühe gemacht habe zu messen.
Bei einem völlig anderen Kontext (einschließlich eines anderen Compilers) ist mir auch aufgefallen, dass die Situation ist ähnlich : Die Option, die die Codegröße anstelle der Geschwindigkeit optimieren soll, optimiert die Codegröße und -geschwindigkeit.
Wenn ich richtig schätze, sind dies Auffüllungen für die Stapelausrichtung.
Nein, das hat nichts mit dem Stack zu tun. Die NOPs, die standardmäßig generiert werden und die die Option -falign - * = 1 verhindern, dienen der Code-Ausrichtung.
Laut Warum funktioniert das GCC-Pad mit NOPs? Es wird in der Hoffnung getan, 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 Ursache ist? Warum und wie?
Es ist sehr wahrscheinlich, dass die Polsterung der Schuldige ist. Der Grund, warum das Auffüllen als notwendig erachtet wird und in einigen Fällen nützlich ist, besteht darin, dass Code normalerweise in Zeilen von 16 Byte abgerufen wird (siehe Optimierungsressourcen von Agner Fog für Details, die je nach Prozessormodell variieren). Das Ausrichten einer Funktion, Schleife oder Beschriftung an einer 16-Byte-Grenze bedeutet, dass die Wahrscheinlichkeit statistisch erhöht ist, dass eine Zeile weniger erforderlich ist, um die Funktion oder Schleife aufzunehmen. Offensichtlich schlägt es fehl, weil diese NOPs die Codedichte und damit die Cache-Effizienz verringern. Im Fall von Schleifen und Beschriftungen müssen die NOPs möglicherweise sogar einmal ausgeführt werden (wenn die Ausführung im Gegensatz zu einem Sprung normal zu der Schleife/Beschriftung gelangt).
Wenn Ihr Programm durch den CODE L1-Cache begrenzt ist, beginnt sich die Größenoptimierung plötzlich auszuzahlen.
Als ich das letzte Mal nachgesehen habe, ist der Compiler nicht schlau genug, um das in allen Fällen herauszufinden.
In Ihrem Fall generiert -O3 wahrscheinlich genug Code für zwei Cache-Zeilen, aber -Os passt in eine Cache-Zeile.
Ich bin kein Experte auf diesem Gebiet, aber ich scheine mich zu erinnern, dass moderne Prozessoren ziemlich empfindlich sind, wenn es um Verzweigungsvorhersage geht. Die Algorithmen, die zur Vorhersage der Verzweigungen verwendet werden (oder waren zumindest in den Tagen, als ich Assembler-Code schrieb), basieren auf mehreren Eigenschaften des Codes, einschließlich der Entfernung eines Ziels und der Richtung.
Das denkbare Szenario sind kleine Schleifen. Wenn der Zweig rückwärts lief und die Entfernung nicht zu groß war, wurde die Zweigvorhersage für diesen Fall optimiert, da alle kleinen Schleifen auf diese Weise ausgeführt werden. Dieselben Regeln können zum Tragen kommen, wenn Sie die Position von add
und work
im generierten Code vertauschen oder wenn sich die Position von beiden geringfügig ändert.
Das heißt, ich habe keine Ahnung, wie ich das überprüfen soll, und ich wollte Sie nur wissen lassen, dass dies etwas sein könnte, worüber Sie nachdenken möchten.