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 ( -O2
ou -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 -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'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 -Os
et -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 -O2
et j'ai fusionné toutes ses différences dans l'assemblage, à l' -Os
exception des .p2align
lignes, 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 .p2align
sont 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=native
comme 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 -Os
de 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 -O2
puis déplacer manuellement l'assemblageadd()
aprèswork()
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 stat
et perf report
n'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 -e
peut cracher sur ma machine; pas seulement les statistiques qui sont données ci-dessus.
Pour le même exécutable, la 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 é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.