GNU/Linux >> Tutoriels Linux >  >> Linux

Quel est l'avantage de __builtin_expect de GCC dans les instructions if else ?

Imaginez le code assembleur qui serait généré à partir de :

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

Je suppose que ça 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 disposées dans un ordre tel que le bar la casse précède le foo cas (par opposition au code C). Cela peut mieux utiliser le pipeline CPU, car un saut écrase les instructions déjà récupérées.

Avant que le saut ne soit exécuté, les instructions en dessous (le bar cas) sont poussés vers le pipeline. Depuis le foo cas est peu probable, sauter aussi est peu probable, donc écraser le pipeline est peu probable.


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 !

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 le puts puis retq retour.

Avec __builtin_expect

Remplacez maintenant if (i) avec :

if (__builtin_expect(i, 0))

et on obtient :

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 reviens !

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 s'exécute plus rapidement avec __builtin_expect que sans, les processeurs sont vraiment intelligents ces jours-ci. Mes tentatives naïves sont là.

C++20 [[likely]] et [[unlikely]]

C++20 a standardisé ces éléments intégrés C++ :Comment utiliser l'attribut probable/improbable de C++20 dans l'instruction if-else Ils feront probablement (un jeu de mots !) La même chose.


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

Je suppose que quelqu'un pensait qu'il était intelligent et qu'il accélérait 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 aurait bien pu empirer les choses. La documentation dit même :

En général, vous devriez préférer utiliser les commentaires de profil réels pour cela (-fprofile-arcs ), car les programmeurs sont notoirement mauvais pour prédire les performances réelles 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 rencontrez un réel problème de performances
  • Vous avez déjà correctement optimisé les algorithmes du système
  • Vous disposez de données de performances pour étayer votre affirmation selon laquelle un cas particulier est le plus probable

Linux
  1. Linux vs Unix :Quelle est la différence ?

  2. Comment installer Gcc 4.7 ?

  3. Mettre à jour Gcc vers la version 6.3 ?

  4. Quelle est la signification de *nix ?

  5. Quelle est la signification de l'avertissement :Lier la bibliothèque partagée à la bibliothèque statique n'est pas portable ?

Qu'est-ce que le Shell sous Linux ?

Qu'est-il arrivé à l'espace libre ?

iptables vs nftables :quelle est la différence ?

Qu'est-ce que la commande kill sous Linux ?

Qu'est-ce que la vulnérabilité Logjam ?

Qu'est-ce que l'utilisateur debian-+ ?