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


445

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 ( -O2oder -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 -O2oder 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 -Osund -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 -O2alle Baugruppen mit -Os Ausnahme der .p2alignLinien 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-loopsan gcc werden alle .p2alignaus 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=nativewie 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 -Osaus 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 -O2dann manuelles Verschieben der Baugruppe add()nach work()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 statund perf reportmacht 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 -eauf meiner Maschine ausspucken kann; nicht nur die Statistiken, die oben angegeben sind.

Für dieselbe ausführbare Datei stalled-cycles-frontendzeigt die lineare Korrelation mit der Ausführungszeit; Ich habe nichts anderes bemerkt, was so klar korrelieren würde. (Das Vergleichen stalled-cycles-frontendverschiedener 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.


36
Blinde Vermutung: Kann dies ein Cache-Miss sein?

@ H2CO3 Das war auch mein erster Gedanke, wurde aber nicht genug ermutigt, um den Kommentar zu veröffentlichen, ohne die Frage des OP gründlich zu lesen und zu verstehen.
πάντα ῥεῖ

2
@ g-makulik Deshalb habe ich gewarnt, dass es eine "blinde Vermutung" ist ;-) "TL; DR" ist für schlechte Fragen reserviert. : P

3
Nur ein interessanter Datenpunkt: Ich finde, dass -O3 oder -Ofast ungefähr 1,5x so schnell ist wie -Os, wenn ich dies mit clang unter OS X kompiliere. (Ich habe nicht versucht, mit gcc zu reproduzieren.)
Rob Napier

2
Es ist der gleiche Code. Schauen Sie sich die Adresse von .L3 genauer an. Falsch ausgerichtete Verzweigungsziele sind teuer.
Hans Passant

Antworten:


503

Standardmäßig optimieren Compiler für "durchschnittliche" Prozessoren. Da verschiedene Prozessoren unterschiedliche Befehlssequenzen bevorzugen, können Compiler-Optimierungen, die durch aktiviert werden -O2, dem durchschnittlichen Prozessor zugute kommen, aber die Leistung Ihres speziellen Prozessors verringern (und das gilt auch für -Os). Wenn Sie dasselbe Beispiel auf verschiedenen Prozessoren ausprobieren, werden Sie feststellen, dass einige davon profitieren, -O2während andere für -OsOptimierungen günstiger sind .

Hier sind die Ergebnisse für time ./test 0 0mehrere 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 verringern, indem Sie gccnach einer Optimierung für Ihren bestimmten Prozessor fragen (mithilfe von Optionen -mtune=nativeoder -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 Ivy - Bridge-Core i3 drei Versionen gcc( 4.6.4, 4.7.3, und 4.8.1) produzieren Binärdateien mit signifikant unterschiedlicher Leistung, aber der Assembler - Code hat nur subtile Variationen. Bisher habe ich keine Erklärung für diese Tatsache.

Zusammenbau 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

Zusammenbau 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

Zusammenbau 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

186
Um es klar zu machen: Haben Sie tatsächlich die Leistung des OP-Codes auf 12 verschiedenen Plattformen gemessen? (+1 für den bloßen Gedanken, dass Sie das tun würden)
Anatolyg

194
@anatolyg Ja, ich habe! (und wird bald einige weitere hinzufügen)
Marat Dukhan

43
Tatsächlich. Ein weiteres +1, um nicht nur über verschiedene CPUs zu theoretisieren, sondern es auch tatsächlich zu beweisen . Nicht etwas (leider), das Sie in jeder Antwort bezüglich der Geschwindigkeit sehen. Werden diese Tests mit demselben Betriebssystem ausgeführt? (Wie es möglich sein könnte, verzerrt dies das Ergebnis ...)
usr2564301

7
@Ali On AMD-FX 6300 verkürzt die -O2 -fno-align-functions -fno-align-loopsZeit auf 0.340s, sodass dies durch Ausrichtung erklärt werden kann. Die optimale Ausrichtung hängt jedoch vom Prozessor ab: Einige Prozessoren bevorzugen ausgerichtete Schleifen und Funktionen.
Marat Dukhan

13
@Jongware Ich sehe nicht, wie das Betriebssystem die Ergebnisse signifikant beeinflussen würde; Die Schleife führt niemals Systemaufrufe durch.
Ali

186

Mein Kollege hat mir geholfen, eine plausible Antwort auf meine Frage zu finden. Er bemerkte die Bedeutung der 256-Byte-Grenze. Er ist hier nicht registriert und hat mich ermutigt, die Antwort selbst zu posten (und den ganzen Ruhm zu nehmen).


Kurze Antwort:

Ist es die Polsterung, die in diesem Fall die Schuld trägt? Warum und wie?

Alles läuft auf die Ausrichtung hinaus. Ausrichtungen können einen erheblichen Einfluss auf die Leistung haben. Deshalb haben wir -falign-*in erster Linie die Flags.

Ich habe den gcc-Entwicklern einen (falschen?) Fehlerbericht übermittelt . Es stellt sich heraus, dass das Standardverhalten lautet: "Wir richten Schleifen standardmäßig auf 8 Byte aus, versuchen jedoch, sie auf 16 Byte auszurichten, wenn wir nicht mehr als 10 Byte ausfüllen müssen." Anscheinend ist diese Standardeinstellung in diesem speziellen Fall und auf meinem Computer nicht die beste Wahl. Clang 3.4 (Trunk) mit -O3führt die entsprechende Ausrichtung durch und der generierte Code zeigt dieses seltsame Verhalten nicht.

Wenn eine unangemessene Ausrichtung vorgenommen wird, wird dies natürlich noch schlimmer. Eine unnötige / schlechte Ausrichtung verbraucht nur ohne Grund Bytes und erhöht möglicherweise die Cache-Fehler usw.

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 ++ - Quellcodes durchführe?

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:

  • Eine XXByte-Grenze schneidet add()in der Mitte ( XXmaschinenabhängig).

  • wenn der Aufruf von add()über eine XXBytegrenze springen muss und das Ziel nicht ausgerichtet ist.

  • wenn add()nicht ausgerichtet ist.

  • wenn die Schleife nicht ausgerichtet ist.

Die ersten beiden sind auf den Codes und Ergebnissen, die Marat Dukhan freundlicherweise veröffentlicht hat, gut sichtbar . 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   

Eine 256-Byte-Grenze schneidet add()genau in der Mitte und weder add()die Schleife noch die Schleife sind ausgerichtet. Überraschung, Überraschung, das ist der langsamste Fall!

In dem Fall 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()wird sie 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 der schnellste von allen dreien. Warum die 256-Byte-Grenze auf seinem Computer besonders wichtig ist, überlasse ich ihm, um es herauszufinden. Ich habe keinen solchen Prozessor.

Jetzt bekomme ich auf meinem Computer diesen 256-Byte-Randeffekt nicht. Nur die Funktion und die Schleifenausrichtung werden auf meinem Computer aktiviert. Wenn ich passiere, g++ -O2 -falign-functions=16 -falign-loops=16ist alles wieder normal: Ich bekomme immer den schnellsten Fall und die Zeit reagiert nicht -fno-omit-frame-pointermehr auf die Flagge. Ich kann g++ -O2 -falign-functions=32 -falign-loops=32oder ein Vielfaches von 16 bestehen, der Code ist auch dafür nicht empfindlich.

Ich habe 2009 zum ersten Mal festgestellt, dass gcc (zumindest bei meinen Projekten und auf meinen Computern) 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 gefragt 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 Flaggen (vorbei -Osstatt -O2) wurden diese Hotspots aus Versehen auf glückliche Weise ausgerichtet und der Code wurde schneller. Es hatte nichts mit Größenoptimierung zu tun: Es war ein Zufall, dass die Hotspots besser ausgerichtet wurden. Von nun an werde ich die Auswirkungen der Ausrichtung auf meine Projekte überprüfen.

Oh, und noch etwas. Wie können solche Hotspots entstehen, wie im Beispiel gezeigt? Wie kann das Inlining einer so winzigen 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 zusammengestellt als : g++ -O2 add.cpp main.cpp.

      gcc wird nicht inline add()!

Das ist alles, es ist so einfach, unbeabsichtigt Hotspots wie den im OP zu erstellen. Natürlich ist es teilweise meine Schuld: gcc ist ein ausgezeichneter Compiler. Wenn die Kompilierung des oben als: g++ -O2 -flto add.cpp main.cpp, das heißt, wenn ich Link Zeitoptimierung durchführen, um den Code läuft in 0.19s!

(Inlining ist im OP künstlich deaktiviert, daher war der Code im OP 2x langsamer).


19
Wow ... Das geht definitiv über das hinaus, was ich normalerweise mache, um Benchmarking-Anomalien zu umgehen.
Mysticial

@Ali Ich denke, das macht Sinn, denn wie kann der Compiler etwas einbinden, das er nicht sieht? Das ist wahrscheinlich der Grund, warum wir die inline+ Funktionsdefinition im Header verwenden. Ich bin mir nicht sicher, wie ausgereift lto in gcc ist. Meine Erfahrung damit zumindest in Mingw ist ein Hit oder Miss.
Urwolf

7
Ich denke, es war Communications of the ACM, das vor einigen Jahren einen Artikel über das Ausführen ziemlich großer Anwendungen (Perl, Spice usw.) veröffentlichte, während das gesamte Binärbild byteweise unter Verwendung von Linux-Umgebungen unterschiedlicher Größe verschoben wurde. Ich erinnere mich an eine typische Varianz von etwa 15%. Ihre Zusammenfassung war, dass viele Benchmark-Ergebnisse nutzlos sind, weil diese externe Variable der Ausrichtung nicht berücksichtigt wird.
Gene

1
besonders für -flto. Es ist ziemlich revolutionär, wenn Sie es noch nie benutzt haben, aus Erfahrung :)
underscore_d

2
Dies ist ein fantastisches Video, das darüber spricht, wie sich die Ausrichtung auf die Leistung auswirken kann und wie man sie profiliert: youtube.com/watch?time_continue=1&v=r-TLSBdHe1A
Zhro

73

Ich füge diese Nachannahme 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 ist auch in CACM erschienen), wie Änderungen der Verbindungsreihenfolge und der Größe der Betriebssystemumgebung allein ausreichten, um die Leistung signifikant zu verändern. 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!" sagt, dass eine versehentliche experimentelle Verzerrung aufgrund nahezu unkontrollierbarer Unterschiede in den Programmlaufumgebungen wahrscheinlich viele Benchmark-Ergebnisse bedeutungslos macht.

Ich denke, Sie stoßen bei derselben Beobachtung auf einen anderen Blickwinkel.

Für leistungskritischen Code ist dies ein ziemlich gutes Argument für Systeme, die die Umgebung bei der Installation oder Laufzeit bewerten und unter den unterschiedlich optimierten Versionen der wichtigsten Routinen die lokal beste auswählen.


33

Ich denke, dass Sie das gleiche Ergebnis erzielen können wie Sie:

Ich habe die Assembly für -O2 gepackt und alle Unterschiede in der Assembly für -Os zusammengeführt, mit Ausnahme der .p2align-Zeilen:

… Mit -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Ich habe -O215 Jahre lang alles mit diesen Optionen zusammengestellt, die jedes Mal, wenn ich mich bemühte zu messen, schneller als normal waren.

Auch für einen völlig anderen Kontext (einschließlich eines anderen Compilers) habe ich festgestellt, dass die Situation ähnlich ist : Die Option, die "Codegröße statt Geschwindigkeit optimieren" soll, optimiert die Codegröße und -geschwindigkeit.

Wenn ich richtig denke, sind dies Auffüllungen für die Stapelausrichtung.

Nein, dies hat nichts mit dem Stapel zu tun. Die standardmäßig generierten NOPs und die Optionen -falign - * = 1 verhindern die Code-Ausrichtung.

Laut Warum funktioniert das GCC-Pad mit NOPs? Es wird in der Hoffnung gemacht, dass der Code schneller läuft, aber anscheinend schlug diese Optimierung in meinem Fall fehl.

Ist es die Polsterung, die in diesem Fall die Schuld trägt? 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 ( Einzelheiten zu den Optimierungsressourcen von Agner Fog , 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 zu enthalten. 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 normal zur Schleife / Beschriftung gelangt, im Gegensatz zu einem Sprung).


Das Lustige ist: -O2 -fno-omit-frame-pointerist genauso gut wie -Os. Bitte überprüfen Sie die aktualisierte Frage.
Ali

11

Wenn Ihr Programm durch den CODE L1-Cache begrenzt ist, zahlt sich die Größenoptimierung plötzlich aus.

Als ich das letzte Mal nachgesehen habe, ist der Compiler nicht klug genug, um dies 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.


1
Wie viel möchten Sie wetten, dass sich diese align = -Parameter auf die Größe der Cache-Zeilen beziehen?
Joshua

Es ist mir eigentlich egal: Es ist auf meinem Computer nicht sichtbar. Und durch das Übergeben der -falign-*=16Flaggen ist alles wieder normal, alles verhält sich konsequent. Für mich ist diese Frage gelöst.
Ali

7

Ich bin kein Experte auf diesem Gebiet, aber ich scheine mich daran zu erinnern, dass moderne Prozessoren sehr empfindlich sind, wenn es um die Vorhersage von Zweigen geht . Die Algorithmen zur Vorhersage der Verzweigungen basieren (oder waren zumindest in den Tagen, als ich Assembler-Code schrieb) auf mehreren Eigenschaften des Codes, einschließlich der Entfernung eines Ziels und der Richtung.

Das Szenario, das mir in den Sinn kommt, sind kleine Schleifen. Wenn der Zweig rückwärts ging und die Entfernung nicht zu weit war, wurde die Verzweigungsvorhersage für diesen Fall optimiert, da alle kleinen Schleifen auf diese Weise ausgeführt werden. Die gleichen Regeln können zum Tragen kommen, wenn Sie die Position von addund workim generierten Code tauschen oder wenn sich die Position von beiden geringfügig ändert.

Trotzdem habe ich keine Ahnung, wie ich das überprüfen soll, und ich wollte Sie nur wissen lassen, dass dies etwas sein könnte, das Sie untersuchen möchten.


Vielen Dank. Ich habe damit gespielt: Ich bekomme eine Beschleunigung nur durch Tauschen add()und work()wenn -O2bestanden wird. In allen anderen Fällen wird der Code durch Tauschen erheblich langsamer. Während des Wochenendes analysierte ich auch Statistiken zur Verzweigungsvorhersage / Fehlvorhersage mit perfund bemerkte nichts, was dieses seltsame Verhalten erklären könnte. Das einzige konsistente Ergebnis ist, dass im langsamen Fall perf100,0 Zoll add()und ein großer Wert in der Zeile direkt nach dem Aufruf von add()in der Schleife gemeldet werden. Es sieht so aus, als ob wir aus irgendeinem Grund add()im langsamen Fall stehen bleiben, aber nicht in den schnellen Läufen.
Ali

Ich denke darüber nach, Intels VTune auf einem meiner Computer zu installieren und selbst ein Profiling durchzuführen. perfunterstützt nur eine begrenzte Anzahl von Dingen, vielleicht ist Intels Zeug auf dem eigenen Prozessor etwas praktischer.
Ali
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.