web-dev-qa-db-fra.com

Pourquoi GCC génère-t-il du code 15 à 20% plus rapide si j'optimise la taille plutôt que la vitesse?

J'ai remarqué pour la première fois en 2009 que GCC (du moins sur mes projets et sur mes machines) avait tendance à générer un code sensiblement plus rapide si j'optimisais pour taille (_-Os_) au lieu de la vitesse (_-O2_ ou _-O3_), et je me demande depuis pourquoi.

J'ai réussi à créer un code (plutôt stupide) qui montre ce comportement surprenant et est suffisamment petit pour être posté 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 _-O2_ ou _-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'assemblage vers GitHub : ils ont rendu la publication gonflée et ont apparemment ajouté très peu de valeur aux questions, car les drapeaux _fno-align-*_ ont la même effet.)

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

Malheureusement, ma compréhension de l’Assemblée étant très limitée, je ne sais donc pas si ce que j’ai fait ensuite était correct: j’ai saisi l’Assemblée pour _-O2_ et fusionné toutes ses différences dans l’Assemblée pour _-Os_ sauf les lignes _.p2align_, résultat ici . Ce code fonctionne toujours dans 0.38s et la seule différence est le _.p2align_ stuff.

Si je devine bien, ce sont des rembourrages pour l'alignement de la pile. Selon Pourquoi le pad GCC fonctionne-t-il avec des NOP? c'est fait dans l'espoir que le code fonctionnera plus vite, 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 pratiquement impossible la micro-optimisation du minutage.

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


PDATE:

Après réponse de Pascal Cuoq , j'ai bricolé un peu avec les alignements. En passant _-O2 -fno-align-functions -fno-align-loops_ à gcc, tous les _.p2align_ sont supprimés de l'assembly et l'exécutable généré s'exécute en 0.38s. Selon le documentation gcc :

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

_  -falign-functions  -falign-jumps  -falign-loops <br/>
  -falign-labels  -freorder-blocks  -freorder-blocks-and-partition <br/>
  -fprefetch-loop-arrays <br/>
_

Cela ressemble donc à un problème de (mauvais) alignement.

Je suis toujours sceptique à propos de _-march=native_ comme suggéré dans réponse de Marat Dukhan . Je ne suis pas convaincu qu'il ne s'agit pas seulement d'intervenir dans ce problème de (mauvais) alignement; cela n'a absolument aucun effet sur ma machine. (Néanmoins, j'ai voté sa réponse.)


PDATE 2:

Nous pouvons enlever _-Os_ de la photo. Les temps suivants sont obtenus en compilant avec

  • _-O2 -fno-omit-frame-pointer_ 0.37s

  • _-O2 -fno-align-functions -fno-align-loops_ 0.37s

  • _-S -O2_ puis déplacez manuellement l'assemblage de add() après work() 0.37s

  • _-O2_ 0.44s

Il me semble que la distance de add() du site d’appel compte beaucoup. J'ai essayé perf, mais les sorties de _perf stat_ et _perf report_ ont très peu de sens pour moi. Cependant, je n'ai pu obtenir qu'un seul 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 nous en tenions à l'appel de add() dans le cas lent.

J'ai examiné tout que _perf -e_ peut cracher sur ma machine; pas seulement les statistiques données ci-dessus.

Pour le même exécutable, le _stalled-cycles-frontend_ montre 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-frontend_ pour différents exécutables n'a pas de sens pour moi.)

J'ai inclus les données manquantes de la mémoire cache car elles apparaissaient dans le premier commentaire. J'ai examiné tous les échecs de mémoire cache qui peuvent être mesurés sur ma machine par perf, pas seulement ceux indiqués ci-dessus. Les erreurs de cache sont très très bruyantes et ne montrent aucune corrélation avec les temps d'exécution.

419
Ali

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 enregistré ici et m'a encouragé à poster la réponse moi-même (et à prendre toute la gloire).


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 drapeaux -falign-* dans le premier endroit.

J'ai soumis n rapport de bogue (fictif?) 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 paramètre par défaut n’est pas le meilleur choix dans ce cas particulier et sur ma machine. Clang 3.4 (trunk) avec -O3 effectue l'alignement approprié et le code généré ne présente pas ce comportement étrange.

Bien sûr, si un alignement inapproprié est effectué, cela aggrave les choses. Un alignement inutile/incorrect ne fait que manger des octets sans raison et peut potentiellement augmenter les erreurs de cache. , etc.

Le bruit qu’il fait rend pratiquement impossible la micro-optimisation du minutage.

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 pile) sur des codes sources C ou C++?

Simplement en disant à gcc de faire le bon alignement:

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


Réponse longue:

Le code fonctionnera plus lentement si:

  • une limite XX d'octets coupe add() au milieu (XX dépend de la machine).

  • si l'appel de add() doit sauter par-dessus une limite d'octet XX 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 résultats 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() juste au milieu et ni add() ni la boucle ne sont alignés. Surprise, surprise, c'est le cas le plus lent!

Dans le cas où gcc-4.7.3 -Os (s’exécute en 0,822 seconde), la limite de 256 octets coupe uniquement dans une section froide (mais ni la boucle, ni add() ne sont coupés):

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 franchir la limite de 256 octets. Ce code est le deuxième plus lent.

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

  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-elle spécifique sur sa machine, je lui laisse le soin de la comprendre. Je n'ai pas un tel processeur.

Maintenant, sur ma machine, je n'ai pas cet effet de frontière de 256 octets. Seules la fonction et l'alignement de la boucle entrent en jeu sur ma machine. Si je réussis g++ -O2 -falign-functions=16 -falign-loops=16 alors tout est rentré dans l'ordre: j'obtiens toujours le cas le plus rapide et le temps n'a plus d'importance pour le drapeau -fno-omit-frame-pointer. Je peux passer g++ -O2 -falign-functions=32 -falign-loops=32 ou n'importe quel multiple de 16, le code n'est pas sensible à cela non plus.

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

Une explication probable est que j'avais des points chauds sensibles à l'alignement, tout comme celui de cet exemple. En jouant avec les drapeaux (en passant -Os au lieu de -O2), ces points chauds ont été alignés de manière chanceuse par accident et le code est devenu plus rapide. Cela n’a rien à voir avec l’optimisation de la taille: c’est par hasard que les zones sensibles se sont mieux alignées. À partir de maintenant, je vérifierai les effets de alignement sur mes projets.

Oh, et encore une chose. Comment de tels points chauds peuvent-ils survenir, comme celui présenté dans l'exemple? Comment l'inline d'une telle fonction minuscule telle que add() peut-elle échouer?

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 suit: g++ -O2 add.cpp main.cpp.

gcc ne sera pas en ligne add()!

C'est tout, il est si facile de créer involontairement des points chauds comme celui du PO. Bien sûr, c'est en partie de ma faute: gcc est un excellent compilateur. Si compilez ce qui précède comme: g++ -O2 -flto add.cpp main.cpp, c'est-à-dire, si j'effectue une optimisation du temps de liaison, le code est exécuté en 0.19!

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

172
Ali

Par défaut, les compilateurs optimisent le processeur "moyen". Étant donné que différents processeurs préfèrent des séquences d'instructions différentes, les optimisations du compilateur activées par -O2 peuvent être avantageuses pour le processeur moyen, mais réduisent les performances de votre processeur (il en va de même pour -Os). Si vous essayez le même exemple sur différents processeurs, vous constaterez que certains bénéficient de -O2 tandis que d'autres sont plus favorables aux optimisations de -Os.

Voici les résultats de time ./test 0 0 sur plusieurs processeurs (le temps utilisateur indiqué):

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 les effets d'optimisations défavorables en demandant à gcc de l'optimiser pour votre processeur particulier (à l'aide des options -mtune=native ou -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.3 et 4.8.1) produisent des binaires avec des performances significativement différentes, mais le code d'assemblage n'a variations subtiles. Jusqu'ici, je n'ai aucune explication de ce fait.

Assemblage de gcc-4.6.4 -Os (s'exécute en 0.709 secondes):

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 de gcc-4.7.3 -Os (s'exécute en 0.822 secondes):

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

J'ajoute ce post-accept pour souligner que les effets de l'alignement sur les performances globales des programmes - y compris les grands - ont été étudiés. Par exemple, cet article (et une version de celui-ci est également apparue dans le MCCA) montre que les modifications apportées à l'ordre des liens et à la taille de l'environnement du système d'exploitation suffisaient à elles seules à améliorer considérablement les performances. Ils attribuent cela à l'alignement des "boucles dynamiques".

Cet article, intitulé "Produire des données erronées sans rien faire de mal de manière évidente!" indique que les biais expérimentaux involontaires dus à des différences presque incontrôlables dans les environnements d'exécution de programmes vident probablement de nombreux résultats de référence.

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

Pour le code critique en termes de performances, il s'agit d'un très bon argument pour les systèmes qui évaluent l'environnement au moment de l'installation ou de l'exécution et choisissent le meilleur emplacement local parmi les versions optimisées différemment des routines clés.

68
Gene

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 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. J'ai tout compilé avec ces options, qui étaient plus rapides que plain -O2 à chaque fois que je me donnais la peine de mesurer, depuis 15 ans.

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

Si je devine bien, 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 les options -falign - * = 1 empêchent d’aligner le code.

Selon Pourquoi le pavé GCC fonctionne-t-il avec des NOP? c'est fait dans l'espoir que le code sera exécuté 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 extrait sur des lignes de 16 octets (voir ressources d'optimisation d'Agner Fog pour les détails, qui varient en fonction du modèle de processeur). Aligner une fonction, une boucle ou une étiquette sur une limite de 16 octets signifie qu'il est statistiquement plus probable qu'un minimum de lignes soit nécessaire pour contenir la fonction ou la boucle. Évidemment, cela se retourne contre-sens car ces NOP réduisent la densité du code et donc l'efficacité du cache. Dans le cas des boucles et des libellés, il se peut même que les NOP doivent être exécutés une fois (lorsque l'exécution arrive normalement à la boucle/au libellé, par opposition à un saut).

30
Pascal Cuoq

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

La dernière fois que j'ai vérifié, 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 -Os tient dans une ligne de cache.

11
Joshua

Je ne suis nullement un expert dans ce domaine, mais il me semble me rappeler que les processeurs modernes sont assez sensibles lorsqu'il s'agit de prédiction de branche . Les algorithmes utilisés pour prédire les branches sont (ou du moins remontés à l'époque où j'écrivais du code assembleur) basés sur plusieurs propriétés du code, notamment la distance d'une cible et la direction.

Le scénario qui me vient à l’esprit est de petites boucles. Lorsque la branche effectuait un retour en arrière et que la distance n'était pas trop grande, la prédicition de la branche optimisait pour ce cas car toutes les petites boucles étaient effectuées de cette façon. Les mêmes règles peuvent entrer en jeu lorsque vous permutez l'emplacement de add et work dans 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 simplement vous faire savoir que cela pourrait être quelque chose que vous souhaitez examiner.

7
Daniel Frey