vendredi 21 août 2015

Gcc et link time optimisation

Comme promis, je vous parle un peu de cette récente fonctionnalité de gcc, qui permet au compilateur d'optimiser à travers les unités de compilation. Le compilateur de Microsoft avait cette fonctionnalité depuis bien longtemps, et gcc était donc un petit peu à la traine, mais à chaque release, de nouvelles améliorations sont apportées à ce qui pourrait bien donner un sérieux coup de fouet aux performances (à l'exécution, parce qu'en revanche, les temps de compilation vont exploser...).

L'idée est que plutôt que de compiler chaque unité de compilation vers du code machine, le compilo s'arrête à une représentation moins bas niveau. C'est cette représentation qui est passée à l'éditeur de liens, qui peut alors refaire une passe d'optimisation en ayant l'ensemble des unités de compilation disponibles. La plus évidente des optimisations possibles est d'inliner les fonctions d'une unité de compilation vers une autre unité de compilation.

Pour comprendre ce qui se passe, je vous propose deux programmes. Voici le premier :

// p1.c
int f(int a, int b)
{
  return a + b;
}

int main()
{
  return f(3, 5);
}

Voici le deuxième, divisé en 2 fichiers

// p2.c
int f(int a, int b);

int main()
{
  return f(3, 5);
}

// p2l.c
int f(int a, int b)
{
  return a + b;
}
Compilons le premier programme avec l'optimisation à fond : "gcc -O3 p1.c -o p1". Puis désassemblons le programme avec "objdump -d -S p1". La partie intéressante est :
00000000004003c0 <main>:
  4003c0:       b8 08 00 00 00          mov    $0x8,%eax
  4003c5:       c3                      retq   

Sans surprises, le compilo a bien inliné le tout, et renvoie simplement la valeur calculée 8.

Maintenant, le deuxième programme: "g++ -O3 p2.c p2l.c -o p2". Le code désassemblé montre bien le passage des deux paramètres 5 et 3 suivi d'un appel de fonction, puisque la fonction "main" ne peut pas voir le contenu de la fonction f.

0000000000400480 <main>:
  400480:       be 05 00 00 00          mov    $0x5,%esi
  400485:       bf 03 00 00 00          mov    $0x3,%edi
  40048a:       e9 01 01 00 00          jmpq   400590 <_Z1fii>

0000000000400590 <_Z1fii>:
  400590:       8d 04 37                lea    (%rdi,%rsi,1),%eax
  400593:       c3                      retq   
  400594:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40059b:       00 00 00 
  40059e:       66 90                   xchg   %ax,%ax

Et maintenant, voyons avec la lto: g++ -flto -O3 p2.c p2l.c -o p2_lto. Ah ah! Nous retrouvons quelque chose d'identique au tout premier programme. Le compilateur a en effet été capable d'inliner la fonction f dans main, quand bien même sa définition n'était pas accessible. Ça marche!

0000000000400480 <main>:
  400480:       b8 08 00 00 00          mov    $0x8,%eax
  400485:       c3                      retq   

Aucun commentaire: