Pourquoi GCC génère-t-il un code 15 à 20% plus rapide si j'optimise pour la taille au lieu de la vitesse?

445

J'ai remarqué pour la première fois en 2009 que GCC (au moins sur mes projets et sur mes machines) a tendance à générer un code sensiblement plus rapide si j'optimise pour la taille ( -Os) au lieu de la vitesse ( -O2ou -O3), et je me demande depuis pourquoi.

J'ai réussi à créer (plutôt stupide) du code qui montre ce comportement surprenant et est suffisamment petit pour être publié ici.

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;
}

Si je le compile avec -Os, il faut 0,38 s pour exécuter ce programme, et 0,44 s s'il est compilé avec -O2ou -O3. Ces temps sont obtenus de manière cohérente et pratiquement sans bruit (gcc 4.7.2, x86_64 GNU / Linux, Intel Core i5-3320M).

(Mise à jour: j'ai déplacé tout le code d'assembly vers GitHub : ils ont gonflé le message et fno-align-*n'ont apparemment ajouté que très peu de valeur aux questions car les indicateurs ont le même effet.)

Voici l'assembly généré avec -Oset -O2.

Malheureusement, ma compréhension de l'assemblage est très limitée, donc je n'ai aucune idée si ce que j'ai fait ensuite était correct: j'ai attrapé l'assemblage -O2et j'ai fusionné toutes ses différences dans l'assemblage, à l' -Os exception des .p2alignlignes, résultat ici . Ce code fonctionne toujours en 0.38s et la seule différence est le .p2align truc.

Si je suppose correctement, ce sont des rembourrages pour l'alignement de la pile. Selon Pourquoi le pad GCC fonctionne-t-il avec les NOP? cela se fait dans l'espoir que le code fonctionnera plus rapidement, mais apparemment cette optimisation s'est retournée contre moi.

Est-ce le rembourrage qui est le coupable dans ce cas? Pourquoi et comment?

Le bruit qu'il fait rend la synchronisation des micro-optimisations impossible.

Comment puis-je m'assurer que ces alignements chanceux / malchanceux accidentels n'interfèrent pas lorsque je fais des micro-optimisations (sans rapport avec l'alignement de la pile) sur le code source C ou C ++?


MISE À JOUR:

Suite à la réponse de Pascal Cuoq, j'ai bricolé un peu les alignements. En passant -O2 -fno-align-functions -fno-align-loopsà gcc, tous .p2alignsont partis de l'assembly et l'exécutable généré s'exécute en 0.38s. Selon la documentation de gcc :

-Os active toutes les optimisations de -O2 [mais] -Os désactive les indicateurs d'optimisation suivants:

  -falign-functions  -falign-jumps  -falign-loops
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition
  -fprefetch-loop-arrays

Donc, cela ressemble à peu près à un (mauvais) alignement.

Je reste sceptique, -march=nativecomme le suggère la réponse de Marat Dukhan . Je ne suis pas convaincu que cela n'interfère pas seulement avec ce (mauvais) problème d'alignement; cela n'a absolument aucun effet sur ma machine. (Néanmoins, j'ai surévalué sa réponse.)


MISE À JOUR 2:

Nous pouvons retirer -Osde l'image. Les temps suivants sont obtenus en compilant avec

  • -O2 -fno-omit-frame-pointer 0,37 s

  • -O2 -fno-align-functions -fno-align-loops 0,37 s

  • -S -O2puis déplacer manuellement l'assemblage add()après work()0,37 s

  • -O2 0,44 s

Il me semble que la distance par add()rapport au site d'appel compte beaucoup. J'ai essayé perf, mais la sortie de perf statet perf reportn'a que très peu de sens pour moi. Cependant, je n'ai pu en obtenir qu'un résultat cohérent:

-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

Pour 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
       ¦    }

Pour -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

Il semble que nous retardions l'appel add()dans le cas lent.

J'ai examiné tout ce qui perf -epeut cracher sur ma machine; pas seulement les statistiques qui sont données ci-dessus.

Pour le même exécutable, la stalled-cycles-frontendmontre une corrélation linéaire avec le temps d'exécution; Je n'ai rien remarqué d'autre qui puisse corréler aussi clairement. (Comparer stalled-cycles-frontendpour différents exécutables n'a pas de sens pour moi.)

J'ai inclus les échecs de cache lors de son premier commentaire. J'ai examiné toutes les erreurs de cache qui peuvent être mesurées sur ma machine perf, pas seulement celles indiquées ci-dessus. Les échecs de cache sont très très bruyants et montrent peu ou pas de corrélation avec les temps d'exécution.

Ali
la source
36
Devinette aveugle: cela peut-il être un échec de cache?
@ H2CO3 C'était aussi ma première pensée, mais ce n'était pas assez encouragé pour poster le commentaire sans avoir lu et compris la question du PO en profondeur.
πάντα ῥεῖ
2
@ g-makulik C'est pourquoi j'ai prévenu que c'était une "supposition aveugle" ;-) "TL; DR" est réservé aux mauvaises questions. : P
3
Juste un point de données intéressant: je trouve que -O3 ou -Ofast est environ 1,5 fois plus rapide que -Os quand je le compile avec clang sur OS X. (Je n'ai pas essayé de reproduire avec gcc.)
Rob Napier
2
C'est le même code. Examinez de plus près l'adresse de .L3, les cibles de succursales mal alignées coûtent cher.
Hans Passant

Réponses:

504

Par défaut, les compilateurs optimisent pour le processeur "moyen". Étant donné que différents processeurs favorisent différentes séquences d'instructions, les optimisations du compilateur activées par -O2peuvent bénéficier au processeur moyen, mais diminuent les performances sur votre processeur particulier (et la même chose s'applique à -Os). Si vous essayez le même exemple sur différents processeurs, vous constaterez que sur certains d'entre eux en bénéficient -O2alors que d'autres sont plus favorables aux -Osoptimisations.

Voici les résultats pour time ./test 0 0plusieurs processeurs (temps utilisateur rapporté):

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

Dans certains cas, vous pouvez atténuer l'effet des optimisations désavantageuses en demandant gccd'optimiser pour votre processeur particulier (en utilisant des options -mtune=nativeou -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

Mise à jour: sur le Core i3 basé sur Ivy Bridge, trois versions de gcc( 4.6.4, 4.7.3et 4.8.1) produisent des binaires avec des performances sensiblement différentes, mais le code d'assemblage n'a que de subtiles variations. Jusqu'à présent, je n'ai aucune explication de ce fait.

Assemblage à partir de gcc-4.6.4 -Os(s'exécute en 0,709 s):

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

Assemblage à partir de gcc-4.7.3 -Os(s'exécute en 0,822 s):

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

Assemblage à partir de gcc-4.8.1 -Os(s'exécute en 0,994 secondes):

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
Marat Dukhan
la source
186
Juste pour être clair: êtes-vous vraiment allé mesurer les performances du code OP sur 12 plates-formes différentes? (+1 pour la simple pensée que vous feriez cela)
anatolyg
194
@anatolyg Oui, je l'ai fait! (et en ajoutera bientôt plus)
Marat Dukhan
43
En effet. Un autre +1 pour non seulement théoriser sur différents processeurs, mais le prouver . Pas quelque chose (hélas) que vous voyez dans chaque réponse concernant la vitesse. Ces tests sont-ils exécutés avec le même système d'exploitation? (Comme cela pourrait être possible, cela fausserait le résultat ...)
usr2564301
7
@Ali Sur AMD-FX 6300 -O2 -fno-align-functions -fno-align-loops, le temps passe à 0.340s, donc cela pourrait être expliqué par l'alignement. Cependant, l'alignement optimal dépend du processeur: certains processeurs préfèrent les boucles et les fonctions alignées.
Marat Dukhan
13
@Jongware Je ne vois pas comment l'OS pourrait influencer de manière significative les résultats; la boucle ne fait jamais d'appels système.
Ali
186

Mon collègue m'a aidé à trouver une réponse plausible à ma question. Il a remarqué l'importance de la limite de 256 octets. Il n'est pas inscrit ici et m'a encouragé à poster la réponse moi-même (et à prendre toute la renommée).


Réponse courte:

Est-ce le rembourrage qui est le coupable dans ce cas? Pourquoi et comment?

Tout se résume à l'alignement. Les alignements peuvent avoir un impact significatif sur les performances, c'est pourquoi nous avons les -falign-*drapeaux en premier lieu.

J'ai soumis un rapport de bogue (faux?) Aux développeurs de gcc . Il s'avère que le comportement par défaut est "nous alignons les boucles sur 8 octets par défaut mais essayons de l'aligner sur 16 octets si nous n'avons pas besoin de remplir plus de 10 octets". Apparemment, ce défaut n'est pas le meilleur choix dans ce cas particulier et sur ma machine. Clang 3.4 (tronc) avec -O3fait l'alignement approprié et le code généré ne montre pas ce comportement étrange.

Bien sûr, si un alignement inapproprié est effectué, cela empire les choses. Un alignement inutile / mauvais mange juste des octets sans raison et augmente potentiellement les échecs de cache, etc.

Le bruit qu'il fait rend la synchronisation des micro-optimisations impossible.

Comment puis-je m'assurer que ces alignements chanceux / malchanceux accidentels n'interfèrent pas lorsque je fais des micro-optimisations (sans rapport avec l'alignement de la pile) sur les codes source C ou C ++?

Simplement en disant à gcc de faire le bon alignement:

g++ -O2 -falign-functions=16 -falign-loops=16


Longue réponse:

Le code s'exécutera plus lentement si:

  • une XXlimite d'octet coupe add()au milieu ( XXen fonction de la machine).

  • si l'appel à add()doit dépasser une XXlimite d'octets et que la cible n'est pas alignée.

  • si add()n'est pas aligné.

  • si la boucle n'est pas alignée.

Les 2 premiers sont magnifiquement visibles sur les codes et les résultats que Marat Dukhan a gentiment posté . Dans ce cas, gcc-4.8.1 -Os(s'exécute en 0,994 secondes):

00000000004004fd <_ZL3addRKiS0_.isra.0>:
  4004fd:       8d 04 37                lea    eax,[rdi+rsi*1]
  400500:       c3   

une limite de 256 octets coupe add()en plein milieu et ni add()ni la boucle n'est alignée. Surprise, surprise, c'est le cas le plus lent!

Dans le cas gcc-4.7.3 -Os(s'exécute en 0,822 s), la limite de 256 octets ne coupe que dans une section froide (mais ni la boucle, ni la add()coupe):

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>

Rien n'est aligné et l'appel à add()doit dépasser la limite de 256 octets. Ce code est le deuxième plus lent.

Dans le cas gcc-4.6.4 -Os(s'exécute en 0,709 s), bien que rien ne soit aligné, l'appel à add()ne doit pas franchir la limite de 256 octets et la cible est exactement à 32 octets:

  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>

C'est le plus rapide des trois. Pourquoi la limite de 256 octets est spéciale sur sa machine, je vais lui laisser le soin de le comprendre. Je n'ai pas un tel processeur.

Maintenant, sur ma machine, je n'obtiens pas cet effet de limite de 256 octets. Seules la fonction et l'alignement de boucle interviennent sur ma machine. Si je passe, g++ -O2 -falign-functions=16 -falign-loops=16tout redevient normal: j'ai toujours le cas le plus rapide et le temps n'est plus sensible au -fno-omit-frame-pointerdrapeau. Je peux passer g++ -O2 -falign-functions=32 -falign-loops=32ou tout multiple de 16, le code n'y est pas sensible non plus.

J'ai remarqué pour la première fois en 2009 que gcc (au moins sur mes projets et sur mes machines) a tendance à générer du code sensiblement plus rapide si j'optimise pour la taille (-Os) au lieu de la vitesse (-O2 ou -O3) et je me suis demandé depuis pourquoi.

Une explication probable est que j'avais des hotspots qui étaient sensibles à l'alignement, tout comme celui de cet exemple. En jouant avec les drapeaux (en passant à la -Osplace de -O2), ces points chauds ont été alignés de manière chanceuse par accident et le code est devenu plus rapide. Cela n'avait rien à voir avec l'optimisation de la taille: c'était par pur accident que les points chauds s'alignaient mieux. Désormais, je vérifierai les effets de l'alignement sur mes projets.

Oh, et encore une chose. Comment de tels hotspots peuvent-ils apparaître, comme celui illustré dans l'exemple? Comment l'échec d'une fonction aussi minuscule comme l' add()échec?

Considère ceci:

// add.cpp
int add(const int& x, const int& y) {
    return x + y;
}

et dans un fichier séparé:

// 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;
}

et compilé comme: g++ -O2 add.cpp main.cpp.

      gcc ne sera pas en ligne add()!

C'est tout, c'est aussi simple que cela de créer involontairement des hotspots comme celui de l'OP. Bien sûr, c'est en partie de ma faute: gcc est un excellent compilateur. Si vous compilez ce qui précède en tant g++ -O2 -flto add.cpp main.cppque :, c'est-à-dire si j'effectue une optimisation du temps de liaison, le code s'exécute en 0.19s!

(L'inline est artificiellement désactivé dans l'OP, par conséquent, le code dans l'OP était 2x plus lent).

Ali
la source
19
Wow ... Cela va certainement au-delà de ce que je fais habituellement pour contourner les anomalies de référence.
Mysticial
@Ali Je suppose que cela a du sens car comment le compilateur peut-il incorporer quelque chose qu'il ne voit pas? C'est probablement pourquoi nous utilisons la inlinedéfinition de la fonction + dans l'en-tête. Je ne sais pas à quel point le lto est mature dans gcc. Mon expérience avec au moins à mingw est un succès.
greatwolf
7
Je pense que ce sont les Communications de l'ACM qui ont publié il y a quelques années un article sur l'exécution de grosses applications (perl, Spice, etc.) tout en décalant l'image binaire entière d'un octet à la fois en utilisant des environnements Linux de tailles différentes. Je me souviens d'une variance typique de 15% environ. Leur résumé était que de nombreux résultats de référence sont inutiles car cette variable externe d'alignement n'est pas prise en compte.
Gene
1
particulièrement pour -flto. c'est assez révolutionnaire si vous ne l'avez jamais utilisé auparavant, en parlant d'expérience :)
underscore_d
2
Il s'agit d'une vidéo fantastique qui explique comment l'alignement peut avoir un impact sur les performances et comment en
établir le
73

J'ajoute ce post-accept pour souligner que les effets de l'alignement sur la performance globale des programmes - y compris les grands - ont été étudiés. Par exemple, cet article (et je crois qu'une version de celui-ci est également apparu dans CACM) montre comment les modifications de l'ordre des liens et de la taille de l'environnement du système d'exploitation suffisaient à elles seules à modifier considérablement les performances. Ils attribuent cela à l'alignement des "boucles chaudes".

Ce document, intitulé "Produire des données erronées sans rien faire de mal!" dit qu'un biais expérimental par inadvertance dû à des différences presque incontrôlables dans les environnements d'exécution de programme rend probablement de nombreux résultats de référence dénués de sens.

Je pense que vous rencontrez un angle différent sur la même observation.

Pour le code critique pour les performances, c'est un très bon argument pour les systèmes qui évaluent l'environnement lors de l'installation ou de l'exécution et choisissent le meilleur local parmi les versions différemment optimisées des routines clés.

Gène
la source
33

Je pense que vous pouvez obtenir le même résultat que ce que vous avez fait:

J'ai saisi l'assembly pour -O2 et j'ai fusionné toutes ses différences dans l'assembly pour -Os à l'exception des lignes .p2align:

… En utilisant -O2 -falign-functions=1 -falign-jumps=1 -falign-loops=1 -falign-labels=1. Je compile tout avec ces options, qui étaient plus rapides que tout le temps à -O2chaque fois que je me donnais la peine de mesurer, depuis 15 ans.

De plus, pour un contexte complètement différent (y compris un compilateur différent), j'ai remarqué que la situation est similaire : l'option qui est censée «optimiser la taille du code plutôt que la vitesse» optimise la taille et la vitesse du code.

Si je suppose correctement, ce sont des rembourrages pour l'alignement de la pile.

Non, cela n'a rien à voir avec la pile, les NOP générés par défaut et que les options -falign - * = 1 empêchent sont pour l'alignement du code.

Selon Pourquoi le pad GCC fonctionne-t-il avec les NOP? cela se fait dans l'espoir que le code fonctionnera plus rapidement mais apparemment cette optimisation s'est retournée contre moi.

Est-ce le rembourrage qui est le coupable dans ce cas? Pourquoi et comment?

Il est très probable que le rembourrage soit le coupable. La raison pour laquelle le remplissage est jugé nécessaire et utile dans certains cas est que le code est généralement récupéré en lignes de 16 octets (voir les ressources d'optimisation d'Agner Fog pour les détails, qui varient selon le modèle de processeur). L'alignement d'une fonction, d'une boucle ou d'une étiquette sur une limite de 16 octets signifie que les chances sont statistiquement augmentées qu'une ligne de moins soit nécessaire pour contenir la fonction ou la boucle. De toute évidence, cela se retourne contre car ces NOP réduisent la densité du code et donc l'efficacité du cache. Dans le cas de boucles et d'étiquettes, les NOP peuvent même devoir être exécutés une fois (lorsque l'exécution arrive normalement à la boucle / étiquette, par opposition à un saut).

Pascal Cuoq
la source
La chose drôle est: -O2 -fno-omit-frame-pointerest tout aussi bon que -Os. Veuillez vérifier la question mise à jour.
Ali
11

Si votre programme est limité par le cache CODE L1, l'optimisation de la taille commence soudainement à payer.

Lors de ma dernière vérification, le compilateur n'est pas assez intelligent pour comprendre cela dans tous les cas.

Dans votre cas, -O3 génère probablement suffisamment de code pour deux lignes de cache, mais -O tient dans une seule ligne de cache.

Joshua
la source
1
Combien vous voulez parier que ces paramètres align = sont liés à la taille des lignes de cache?
Joshua
Je m'en fiche vraiment: ce n'est pas visible sur ma machine. Et en passant les -falign-*=16drapeaux, tout revient à la normale, tout se comporte de façon cohérente. En ce qui me concerne, cette question est résolue.
Ali
7

Je ne suis en aucun cas un expert dans ce domaine, mais je semble me souvenir que les processeurs modernes sont assez sensibles en matière de prédiction de branche . Les algorithmes utilisés pour prédire les branches sont (ou du moins étaient à l'époque où j'ai écrit le code assembleur) basés sur plusieurs propriétés du code, y compris la distance d'une cible et la direction.

Le scénario qui me vient à l'esprit est celui des petites boucles. Lorsque la branche reculait et que la distance n'était pas trop éloignée, la prédiction de la branche était optimisée pour ce cas car toutes les petites boucles se font de cette façon. Les mêmes règles peuvent entrer en jeu lorsque vous échangez l'emplacement de addet workdans le code généré ou lorsque la position des deux change légèrement.

Cela dit, je ne sais pas comment vérifier cela et je voulais juste vous faire savoir que cela pourrait être quelque chose que vous souhaitez examiner.

Daniel Frey
la source
Merci. J'ai joué avec: je n'accélère qu'en échangeant add()et work()si -O2c'est passé. Dans tous les autres cas, le code ralentit considérablement en échangeant. Au cours du week-end, j'ai également analysé les statistiques de prédiction / mauvaise prédiction de branche avec perfet je n'ai rien remarqué qui pourrait expliquer ce comportement bizarre. Le seul résultat cohérent est que dans le cas lent, il perfindique 100,0 pouces add()et une grande valeur sur la ligne juste après l'appel à add()dans la boucle. Il semble que nous stagnons pour une raison quelconque add()dans le cas lent mais pas dans les courses rapides.
Ali
Je pense installer le VTune d'Intel sur une de mes machines et faire moi-même un profilage. perfne prend en charge qu'un nombre limité de choses, peut-être que les trucs d'Intel sont un peu plus pratiques sur leur propre processeur.
Ali