web-dev-qa-db-fra.com

Quel est l’avantage de __builtin_expect de GCC dans d’autres déclarations?

Je suis tombé sur un #define Dans lequel ils utilisent __builtin_expect.

La documentation dit:

Fonction intégrée: long __builtin_expect (long exp, long c)

Vous pouvez utiliser __builtin_expect Pour fournir au compilateur des informations de prédiction de branche. En général, vous devriez préférer utiliser les informations de retour de profil réelles pour cela (-fprofile-arcs), Car les programmeurs sont notoirement mal à même de prédire les performances réelles de leurs programmes. Cependant, il existe des applications dans lesquelles ces données sont difficiles à collecter.

La valeur de retour est la valeur de exp, qui devrait être une expression intégrale. La sémantique de la fonction intégrée est qu'il est prévu que exp == c. Par exemple:

      if (__builtin_expect (x, 0))
        foo ();

indiquerait que nous ne prévoyons pas d'appeler foo, puisque nous prévoyons que x sera nul.

Alors pourquoi ne pas utiliser directement:

if (x)
    foo ();

au lieu de la syntaxe compliquée avec __builtin_expect?

128
kingsmasher1

Imaginez le code d'assemblage qui serait généré à partir de:

if (__builtin_expect(x, 0)) {
    foo();
    ...
} else {
    bar();
    ...
}

Je suppose que cela devrait être quelque chose comme:

  cmp   $x, 0
  jne   _foo
_bar:
  call  bar
  ...
  jmp   after_if
_foo:
  call  foo
  ...
after_if:

Vous pouvez voir que les instructions sont organisées dans un ordre tel que le cas bar précède le cas foo (par opposition au code C). Cela permet de mieux utiliser le pipeline du processeur, puisqu'un saut saute les instructions déjà extraites.

Avant que le saut ne soit exécuté, les instructions en dessous (la casse bar sont transférées dans le pipeline). Puisque le cas foo est improbable, il est également improbable de sauter, ce qui rend improbable le pipeline.

161

L'idée de __builtin_expect consiste à indiquer au compilateur que vous constaterez généralement que l'expression est évaluée à c, afin que le compilateur puisse optimiser ce cas.

J'imagine que quelqu'un pensait qu'ils étaient intelligents et qu'ils accéléraient les choses en faisant cela.

Malheureusement, à moins que la situation ne soit très bien comprise (il est probable qu’ils n’aient rien fait de tel), cela pourrait bien avoir aggravé la situation. La documentation dit même:

En général, vous devriez préférer utiliser les commentaires du profil réel pour cela (-fprofile-arcs), les programmeurs sont notoirement mal à même de prédire le fonctionnement réel de leurs programmes. Cependant, il existe des applications dans lesquelles ces données sont difficiles à collecter.

En général, vous ne devriez pas utiliser __builtin_expect sauf si:

  • Vous avez un problème de performance très réel
  • Vous avez déjà optimisé les algorithmes du système de manière appropriée
  • Vous avez des données de performance pour confirmer votre assertion selon laquelle un cas particulier est le plus probable.
40
Michael Kohne

Décompilons pour voir ce que GCC 4.8 en fait

Blagovest a mentionné l'inversion de branche pour améliorer le pipeline, mais les compilateurs actuels le font-ils vraiment? Découvrons-le!

Sans __builtin_expect

#include "stdio.h"
#include "time.h"

int main() {
    /* Use time to prevent it from being optimized away. */
    int i = !time(NULL);
    if (i)
        puts("a");
    return 0;
}

Compiler et décompiler avec GCC 4.8.2 x86_64 Linux:

gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o

Sortie:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       75 0a                   jne    1a <main+0x1a>
  10:       bf 00 00 00 00          mov    $0x0,%edi
                    11: R_X86_64_32 .rodata.str1.1
  15:       e8 00 00 00 00          callq  1a <main+0x1a>
                    16: R_X86_64_PC32       puts-0x4
  1a:       31 c0                   xor    %eax,%eax
  1c:       48 83 c4 08             add    $0x8,%rsp
  20:       c3                      retq

L'ordre des instructions en mémoire était inchangé: d'abord puts et ensuite retq.

Avec __builtin_expect

Maintenant, remplacez if (i) par:

if (__builtin_expect(i, 0))

et nous obtenons:

0000000000000000 <main>:
   0:       48 83 ec 08             sub    $0x8,%rsp
   4:       31 ff                   xor    %edi,%edi
   6:       e8 00 00 00 00          callq  b <main+0xb>
                    7: R_X86_64_PC32        time-0x4
   b:       48 85 c0                test   %rax,%rax
   e:       74 07                   je     17 <main+0x17>
  10:       31 c0                   xor    %eax,%eax
  12:       48 83 c4 08             add    $0x8,%rsp
  16:       c3                      retq
  17:       bf 00 00 00 00          mov    $0x0,%edi
                    18: R_X86_64_32 .rodata.str1.1
  1c:       e8 00 00 00 00          callq  21 <main+0x21>
                    1d: R_X86_64_PC32       puts-0x4
  21:       eb ed                   jmp    10 <main+0x10>

Le puts a été déplacé à la toute fin de la fonction, le retq revient!

Le nouveau code est fondamentalement le même que:

int i = !time(NULL);
if (i)
    goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;

Cette optimisation n'a pas été faite avec -O0.

Mais bonne chance pour écrire un exemple qui tourne plus vite avec __builtin_expect Que sans, les CPU sont vraiment intelligents ces jours-ci . Mes tentatives naïves sont ici .

Comme il est dit dans la description, la première version ajoute un élément prédictif à la construction, indiquant au compilateur que le x == 0 _ est la branche la plus probable - c’est-à-dire que votre programme s’occupera plus souvent.

En gardant cela à l'esprit, le compilateur peut optimiser la condition afin qu'elle nécessite le moins de travail possible lorsque la condition attendue se vérifie, au risque de devoir faire plus de travail en cas de condition imprévue.

Examinez comment les conditions sont implémentées pendant la phase de compilation, ainsi que dans l'assembly résultant, pour voir comment une branche peut nécessiter moins de travail que l'autre.

Cependant, je ne m'attendrais à ce que cette optimisation ait un effet notable si le conditionnel en question fait partie d'une boucle interne étroite appelée "lot", car la différence dans le code résultant est relativement petite. Et si vous l’optimisez dans le mauvais sens, vous risquez de diminuer vos performances.

13
Kerrek SB

Je ne vois aucune des réponses à la question que je pense que vous posiez, paraphrasez:

Existe-t-il un moyen plus portable d'indiquer la prédiction de branche au compilateur?.

Le titre de votre question m'a fait penser à le faire de cette façon:

if ( !x ) {} else foo();

Si le compilateur suppose que 'true' est plus probable, il pourrait optimiser pour ne pas appeler foo().

Le problème ici est simplement que, généralement, vous ne savez pas ce que le compilateur va supposer. Tout code utilisant ce type de technique doit donc être soigneusement mesuré (et éventuellement surveillé dans le temps si le contexte change).

1
nobar

Je le teste sur Mac selon @ Blagovest Buyukliev et @Ciro. Les assemblages semblent clairs et j'ajoute des commentaires;

Les commandes sont gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o

Lorsque j'utilise -O3 ,, l'apparence est la même, peu importe le __builtin_expect (i, 0) existant ou non.

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp     
0000000000000001    movq    %rsp, %rbp    // open function stack
0000000000000004    xorl    %edi, %edi       // set time args 0 (NULL)
0000000000000006    callq   _time      // call time(NULL)
000000000000000b    testq   %rax, %rax   // check time(NULL)  result
000000000000000e    je  0x14           //  jump 0x14 if testq result = 0, namely jump to puts
0000000000000010    xorl    %eax, %eax   //  return 0   ,  return appear first 
0000000000000012    popq    %rbp    //  return 0
0000000000000013    retq                     //  return 0
0000000000000014    leaq    0x9(%rip), %rdi  ## literal pool for: "a"  // puts  part, afterwards
000000000000001b    callq   _puts
0000000000000020    xorl    %eax, %eax
0000000000000022    popq    %rbp
0000000000000023    retq

Lors de la compilation avec -O2, l’apparence est différente avec et sans __builtin_expect (i, 0)

D'abord sans

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    jne 0x1c       //   jump to 0x1c if not zero, then return
0000000000000010    leaq    0x9(%rip), %rdi ## literal pool for: "a"   //   put part appear first ,  following   jne 0x1c
0000000000000017    callq   _puts
000000000000001c    xorl    %eax, %eax     // return part appear  afterwards
000000000000001e    popq    %rbp
000000000000001f    retq

Maintenant avec __builtin_expect (i, 0)

testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    xorl    %edi, %edi
0000000000000006    callq   _time
000000000000000b    testq   %rax, %rax
000000000000000e    je  0x14   // jump to 0x14 if zero  then put. otherwise return 
0000000000000010    xorl    %eax, %eax   // return appear first 
0000000000000012    popq    %rbp
0000000000000013    retq
0000000000000014    leaq    0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b    callq   _puts
0000000000000020    jmp 0x10

Pour résumer, __builtin_expect fonctionne dans le dernier cas.

0
Victor Choy