it-swarm.com.de

Warum generiert GCC 15-20% schnelleren Code, wenn ich die Größe anstelle der Geschwindigkeit optimiere?

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.

419
Ali

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).

172
Ali

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
480
Marat Dukhan

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.

68
Gene

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).

30
Pascal Cuoq

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.

11
Joshua

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.

7
Daniel Frey