33 votes

Pourquoi cette fonction C++ produit-elle tant d'erreurs de prédiction de branche ?

Soit A être un tableau qui contient un nombre impair de zéros et de uns. Si n est la taille de A alors A est construit de telle sorte que le premier ceil(n/2) Les éléments sont 0 et les éléments restants 1 .

Donc si n = 9 , A ressemblerait à ceci :

0,0,0,0,0,1,1,1,1

Le but est de trouver la somme de 1s dans le tableau et nous le faisons en utilisant cette fonction :

s = 0;
void test1(int curIndex){
    //A is 0,0,0,...,0,1,1,1,1,1...,1

    if(curIndex == ceil(n/2)) return;

    if(A[curIndex] == 1) return;

    test1(curIndex+1);
    test1(size-curIndex-1);

    s += A[curIndex+1] + A[size-curIndex-1];

}

Cette fonction est plutôt stupide pour le problème donné, mais c'est une simulation d'une fonction différente que je veux voir ressembler à celle-ci et qui produit la même quantité de mauvaises prédictions de branche.

Voici le code complet de l'expérience :

#include <iostream>
#include <fstream>

using namespace std;

int size;
int *A;
int half;
int s;

void test1(int curIndex){
    //A is 0,0,0,...,0,1,1,1,1,1...,1

    if(curIndex == half) return;
    if(A[curIndex] == 1) return;

    test1(curIndex+1);
    test1(size - curIndex - 1);

    s += A[curIndex+1] + A[size-curIndex-1];

}

int main(int argc, char* argv[]){

    size = atoi(argv[1]);
    if(argc!=2){
        cout<<"type ./executable size{odd integer}"<<endl;
        return 1;
    }
    if(size%2!=1){
        cout<<"size must be an odd number"<<endl;
        return 1;
    }
    A = new int[size];

    half = size/2;
    int i;
    for(i=0;i<=half;i++){
        A[i] = 0;
    }
    for(i=half+1;i<size;i++){
        A[i] = 1;
    }

    for(i=0;i<100;i++) {
        test1(0);
    }
    cout<<s<<endl;

    return 0;
}

Compilez en tapant g++ -O3 -std=c++11 file.cpp et l'exécuter en tapant ./executable size{odd integer} .

J'utilise un processeur Intel(R) Core(TM) i5-3470 à 3,20 GHz avec 8 Go de RAM, cache L1 256 Ko, cache L2 1 Mo, cache L3 6 Mo.

Ejecutar perf stat -B -e branches,branch-misses ./cachetests 111111 me donne ce qui suit :

   Performance counter stats for './cachetests 111111':

    32,639,932      branches                                                    
     1,404,836      branch-misses             #    4.30% of all branches        

   0.060349641 seconds time elapsed

si je supprime la ligne

s += A[curIndex+1] + A[size-curIndex-1];

J'obtiens la sortie suivante de perf :

  Performance counter stats for './cachetests 111111':

    24,079,109      branches                                                    
        39,078      branch-misses             #    0.16% of all branches        

   0.027679521 seconds time elapsed

Qu'est-ce que cette ligne a à voir avec les prédictions de branche alors qu'il ne s'agit même pas d'une instruction if ?

La façon dont je le vois, dans le premier ceil(n/2) - 1 appels de test1() les deux déclarations if seront fausses. Dans le ceil(n/2)-th appeler, if(curIndex == ceil(n/2)) sera vrai. Dans les autres n-ceil(n/2) appelle, la première affirmation sera fausse, et la seconde sera vraie.

Pourquoi Intel ne parvient-il pas à prévoir un comportement aussi simple ?

Examinons maintenant un deuxième cas. Supposons que A a maintenant des zéros et des uns alternés. Nous commencerons toujours à partir de 0. Donc si n = 9 A ressemblera à ceci :

0,1,0,1,0,1,0,1,0

La fonction que nous allons utiliser est la suivante :

void test2(int curIndex){
    //A is 0,1,0,1,0,1,0,1,....
    if(curIndex == size-1) return;
    if(A[curIndex] == 1) return;

    test2(curIndex+1);
    test2(curIndex+2);

    s += A[curIndex+1] + A[curIndex+2];

}

Et voici le code complet de l'expérience :

#include <iostream>
#include <fstream>

using namespace std;

int size;
int *A;
int s;

void test2(int curIndex){
    //A is 0,1,0,1,0,1,0,1,....
    if(curIndex == size-1) return;
    if(A[curIndex] == 1) return;

    test2(curIndex+1);
    test2(curIndex+2);

    s += A[curIndex+1] + A[curIndex+2];

}

int main(int argc, char* argv[]){

    size = atoi(argv[1]);
    if(argc!=2){
        cout<<"type ./executable size{odd integer}"<<endl;
        return 1;
    }
    if(size%2!=1){
        cout<<"size must be an odd number"<<endl;
        return 1;
    }
    A = new int[size];
    int i;
    for(i=0;i<size;i++){
        if(i%2==0){
            A[i] = false;
        }
        else{
            A[i] = true;
        }
    }

    for(i=0;i<100;i++) {
        test2(0);
    }
    cout<<s<<endl;

    return 0;
}

J'exécute perf en utilisant les mêmes commandes que précédemment :

    Performance counter stats for './cachetests2 111111':

    28,560,183      branches                                                    
        54,204      branch-misses             #    0.19% of all branches        

   0.037134196 seconds time elapsed

Et enlever cette ligne a encore amélioré un peu les choses :

   Performance counter stats for './cachetests2 111111':

    28,419,557      branches                                                    
        16,636      branch-misses             #    0.06% of all branches        

   0.009977772 seconds time elapsed

Maintenant, si nous analysons la fonction, if(curIndex == size-1) sera faux n-1 temps, et if(A[curIndex] == 1) passe alternativement de vrai à faux.

Comme je le vois, les deux fonctions devraient être faciles à prévoir, mais ce n'est pas le cas pour la première fonction. En même temps, je ne suis pas sûr de ce qui se passe avec cette ligne et pourquoi elle joue un rôle dans l'amélioration du comportement des branches.

26voto

Roman Khimov Points 21

Voici ce que j'en pense après l'avoir regardé pendant un moment. Tout d'abord, le problème est facilement reproductible avec -O2 il est donc préférable de l'utiliser comme un référence, car il génère un code simple et non déroulé qui est facile à comprendre. facile à analyser. Le problème avec -O3 est essentiellement la même, c'est juste un peu moins évident.

Ainsi, pour le premier cas (motif demi-zéro avec demi-un) le compilateur génère ce code :

 0000000000400a80 <_Z5test1i>:
   400a80:       55                      push   %rbp
   400a81:       53                      push   %rbx
   400a82:       89 fb                   mov    %edi,%ebx
   400a84:       48 83 ec 08             sub    $0x8,%rsp
   400a88:       3b 3d 0e 07 20 00       cmp    0x20070e(%rip),%edi        #
   60119c <half>
   400a8e:       74 4f                   je     400adf <_Z5test1i+0x5f>
   400a90:       48 8b 15 09 07 20 00    mov    0x200709(%rip),%rdx        #
   6011a0 <A>
   400a97:       48 63 c7                movslq %edi,%rax
   400a9a:       48 8d 2c 85 00 00 00    lea    0x0(,%rax,4),%rbp
   400aa1:       00 
   400aa2:       83 3c 82 01             cmpl   $0x1,(%rdx,%rax,4)
   400aa6:       74 37                   je     400adf <_Z5test1i+0x5f>
   400aa8:       8d 7f 01                lea    0x1(%rdi),%edi
   400aab:       e8 d0 ff ff ff          callq  400a80 <_Z5test1i>
   400ab0:       89 df                   mov    %ebx,%edi
   400ab2:       f7 d7                   not    %edi
   400ab4:       03 3d ee 06 20 00       add    0x2006ee(%rip),%edi        #
   6011a8 <size>
   400aba:       e8 c1 ff ff ff          callq  400a80 <_Z5test1i>
   400abf:       8b 05 e3 06 20 00       mov    0x2006e3(%rip),%eax        #
   6011a8 <size>
   400ac5:       48 8b 15 d4 06 20 00    mov    0x2006d4(%rip),%rdx        #
   6011a0 <A>
   400acc:       29 d8                   sub    %ebx,%eax
   400ace:       48 63 c8                movslq %eax,%rcx
   400ad1:       8b 44 2a 04             mov    0x4(%rdx,%rbp,1),%eax
   400ad5:       03 44 8a fc             add    -0x4(%rdx,%rcx,4),%eax
   400ad9:       01 05 b9 06 20 00       add    %eax,0x2006b9(%rip)        #
   601198 <s>
   400adf:       48 83 c4 08             add    $0x8,%rsp
   400ae3:       5b                      pop    %rbx
   400ae4:       5d                      pop    %rbp
   400ae5:       c3                      retq   
   400ae6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
   400aed:       00 00 00 

Très simple, un peu ce à quoi on pourrait s'attendre -- deux branches conditionnelles, deux appels. Il nous donne cette statistique (ou des statistiques similaires) sur Core 2 Duo T6570, AMD Phenom II X4 925 et Core i7-4770 :

$ perf stat -B -e branches,branch-misses ./a.out 111111
5555500

 Performance counter stats for './a.out 111111':

        45,216,754      branches                                                    
         5,588,484      branch-misses             #   12.36% of all branches        

       0.098535791 seconds time elapsed

Si vous devez faire ce changement, déplacez l'affectation avant les appels récursifs :

 --- file.cpp.orig  2016-09-22 22:59:20.744678438 +0300
 +++ file.cpp   2016-09-22 22:59:36.492583925 +0300
 @@ -15,10 +15,10 @@
      if(curIndex == half) return;
      if(A[curIndex] == 1) return;

 +    s += A[curIndex+1] + A[size-curIndex-1];
      test1(curIndex+1);
      test1(size - curIndex - 1);

 -    s += A[curIndex+1] + A[size-curIndex-1];

  }

L'image change :

 $ perf stat -B -e branches,branch-misses ./a.out 111111
 5555500

  Performance counter stats for './a.out 111111':

         39,495,804      branches                                                    
             54,430      branch-misses             #    0.14% of all branches        

        0.039522259 seconds time elapsed

Et oui, comme cela a déjà été noté, c'est directement lié à la récursion de queue parce que si vous compilez le code corrigé avec -fno-optimize-sibling-calls vous obtiendrez les mêmes "mauvais" résultats. Alors regardons regardons ce que nous avons en assemblage avec l'optimisation des appels de queue :

 0000000000400a80 <_Z5test1i>:
   400a80:       3b 3d 16 07 20 00       cmp    0x200716(%rip),%edi        #
   60119c <half>
   400a86:       53                      push   %rbx
   400a87:       89 fb                   mov    %edi,%ebx
   400a89:       74 5f                   je     400aea <_Z5test1i+0x6a>
   400a8b:       48 8b 05 0e 07 20 00    mov    0x20070e(%rip),%rax        #
   6011a0 <A>
   400a92:       48 63 d7                movslq %edi,%rdx
   400a95:       83 3c 90 01             cmpl   $0x1,(%rax,%rdx,4)
   400a99:       74 4f                   je     400aea <_Z5test1i+0x6a>
   400a9b:       8b 0d 07 07 20 00       mov    0x200707(%rip),%ecx        #
   6011a8 <size>
   400aa1:       eb 15                   jmp    400ab8 <_Z5test1i+0x38>
   400aa3:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
   400aa8:       48 8b 05 f1 06 20 00    mov    0x2006f1(%rip),%rax        #
   6011a0 <A>
   400aaf:       48 63 d3                movslq %ebx,%rdx
   400ab2:       83 3c 90 01             cmpl   $0x1,(%rax,%rdx,4)
   400ab6:       74 32                   je     400aea <_Z5test1i+0x6a>
   400ab8:       29 d9                   sub    %ebx,%ecx
   400aba:       8d 7b 01                lea    0x1(%rbx),%edi
   400abd:       8b 54 90 04             mov    0x4(%rax,%rdx,4),%edx
   400ac1:       48 63 c9                movslq %ecx,%rcx
   400ac4:       03 54 88 fc             add    -0x4(%rax,%rcx,4),%edx
   400ac8:       01 15 ca 06 20 00       add    %edx,0x2006ca(%rip)        #
   601198 <s>
   400ace:       e8 ad ff ff ff          callq  400a80 <_Z5test1i>
   400ad3:       8b 0d cf 06 20 00       mov    0x2006cf(%rip),%ecx        #
   6011a8 <size>
   400ad9:       89 c8                   mov    %ecx,%eax
   400adb:       29 d8                   sub    %ebx,%eax
   400add:       89 c3                   mov    %eax,%ebx
   400adf:       83 eb 01                sub    $0x1,%ebx
   400ae2:       39 1d b4 06 20 00       cmp    %ebx,0x2006b4(%rip)        #
   60119c <half>
   400ae8:       75 be                   jne    400aa8 <_Z5test1i+0x28>
   400aea:       5b                      pop    %rbx
   400aeb:       c3                      retq   
   400aec:       0f 1f 40 00             nopl   0x0(%rax)

Il comporte quatre branches conditionnelles avec un appel. Alors analysons les données que nous avons obtenues jusqu'à présent.

Tout d'abord, qu'est-ce qu'une instruction de branchement du point de vue du processeur ? C'est l'une des call , ret , j* (y compris les jmp ) et loop . call y jmp ne sont pas très intuitifs, mais ils sont essentiels pour compter correctement les choses.

Globalement, nous nous attendons à ce que cette fonction soit appelée 11111100 fois, une pour chaque élément, soit environ 11M. Dans la version non optimisée pour les appels de queue, nous voyons environ 45M branches, l'initialisation dans main() est juste 111K, toutes les autres choses sont mineures, donc la contribution principale à ce nombre vient de notre fonction. Notre fonction est call -ed, il évalue la première je ce qui est vrai dans tous les cas sauf un, puis il évalue la deuxième option je ce qui est vrai la moitié du temps, puis elle s'appelle elle-même récursivement (mais nous avons déjà compté que la fonction est invoquée 11M fois) ou revient (comme elle le fait après des appels récursifs. Cela fait donc 4 instructions de branchement pour 11M d'appels, exactement le nombre que nous voyons. Parmi celles-ci, environ 5,5 millions de branchements sont manqués, ce qui suggère que ces manques proviennent tous d'une instruction mal prédite, soit quelque chose qui est évalué 11 millions de fois et qui est manqué environ 50% du temps, soit quelque chose qui est évalué la moitié du temps et qui est toujours manqué.

Qu'avons-nous dans la version optimisée pour les appels de queue ? Nous avons la fonction appelée environ 5,5 millions de fois, mais maintenant chaque invocation coûte une heure et demie. call , deux branches initialement (la première est vraie dans tous les cas sauf un et la seconde est toujours fausse à cause de nos données), puis a jmp puis un appel (mais nous avons déjà compté que nous avons 5.5M d'appels), puis une branche à 400ae8 et une branche à 400ab6 (toujours vrai grâce à nos données), puis retour. Donc, en moyenne, cela fait quatre branches conditionnelles, un saut inconditionnel, un appel et une branche indirecte (retour de la fonction), 5,5M fois 7 nous donne un compte global d'environ 39M branches, exactement comme nous le voyons dans la sortie perf.

Ce que nous savons, c'est que le processeur n'a aucun problème pour prédire les choses dans un flux avec un appel de fonction (même si cette version a plus de branches conditionnelles) et qu'il a des problèmes avec deux appels de fonction. Cela suggère donc que le problème se situe dans les retours de la fonction.

Malheureusement, nous savons très peu de choses sur les détails de la manière dont la branche de nos processeurs modernes. La meilleure analyse que j'ai pu trouver c'est ceci et cela suggère que les processeurs ont un tampon de pile de retour d'environ 16 entrées. Si nous revenons à nos données avec cette découverte en main, les choses commencent à se clarifier un peu.

Quand on a un motif de demi-zéro avec un motif de demi-un, on récidive. très profondément dans test1(curIndex+1) mais ensuite vous commencez à revenir et en appelant test1(size-curIndex-1) . Cette récursion est jamais plus profond qu'un appel, donc les retours sont parfaitement prédits pour cela. Mais rappelez-vous que nous 55555 invocations et que le processeur ne se souvient que des 16 dernières. pas surprenant qu'il ne puisse pas deviner nos retours à partir de 55539 niveaux de profondeur, c'est plus surprenant qu'il puisse le faire avec la version optimisée pour les appels de queue.

En fait, le comportement de la version optimisée par tail-call suggère que l'absence de toute autre information sur les retours, le processeur suppose simplement que le bon est le dernier vu. C'est aussi prouvé par le comportement de version non optimisée pour les appels de queue, parce qu'il y a 55555 appels profonds dans le cycle de vie. test1(curIndex+1) et ensuite, au retour, il s'enfonce toujours un niveau plus profond dans test1(size-curIndex-1) ainsi, lorsque nous passerons de 55555 à 55539 (ou quel que soit le tampon de retour de votre processeur), il fait appel à test1(size-curIndex-1) et il n'y a absolument rien à en tirer. informations sur le prochain retour, donc il suppose que nous devons retourner à la dernière adresse vue (qui est l'adresse à laquelle il faut retourner à partir de la dernière adresse). dernière adresse vue (qui est l'adresse à laquelle il faut retourner à partir de test1(size-curIndex-1) ) et c'est évidemment faux. 55539 fois faux. Avec 100 cycles de la fonction, c'est exactement les 5.5M d'erreurs de prédiction de branchement que nous voyons.

Maintenant, passons à votre motif alternatif et au code correspondant. Ce code est en fait très différent, si vous voulez analyser la façon dont il entre dans la profondeur. Ici, vous avez votre test2(curIndex+1) toujours revenir immédiatement et votre test2(curIndex+2) a toujours aller plus loin. Donc les retours de test2(curIndex+1) sont toujours parfaitement prédits (ils ne vont juste pas profondément assez profondément) et lorsque nous devons terminer notre récursion en test2(curIndex+2) il toujours revient au même point, toutes les 55555 fois, donc le processeur n'a pas de problèmes avec ça.

Cela peut être prouvé par cette petite modification de votre code original de demi-zéro avec demi-zéro :

--- file.cpp.orig       2016-09-23 11:00:26.917977032 +0300
+++ file.cpp    2016-09-23 11:00:31.946027451 +0300
@@ -15,8 +15,8 @@
   if(curIndex == half) return;
   if(A[curIndex] == 1) return;

-  test1(curIndex+1);
   test1(size - curIndex - 1);
+  test1(curIndex+1);

   s += A[curIndex+1] + A[size-curIndex-1];

Le code généré n'est toujours pas optimisé pour les appels de queue (en termes d'assemblage, il est très similaire à l'original), mais vous obtenez quelque chose comme ceci dans la sortie de perf :

$ perf stat -B -e branches,branch-misses ./a.out 111111 
5555500

 Performance counter stats for './a.out 111111':

        45 308 579      branches                                                    
            75 927      branch-misses             #    0,17% of all branches        

       0,026271402 seconds time elapsed

Comme prévu, notre premier appel revient toujours immédiatement et le second va à 55555 mètres de profondeur et ne revient qu'au même point.

Maintenant que cela est résolu, laissez-moi vous montrer quelque chose dans ma manche. Sur un système, et c'est le Core i5-5200U, la version originale non optimisée de demi-zéro avec demi-un montre ce résultat :

 $ perf stat -B -e branches,branch-misses ./a.out 111111
 5555500

  Performance counter stats for './a.out 111111':

         45 331 670      branches                                                    
             16 349      branch-misses             #    0,04% of all branches        

        0,043351547 seconds time elapsed

Donc, apparemment, Broadwell peut gérer ce modèle facilement, ce qui nous ramène à la question de savoir ce que nous savons de la logique de prédiction de branchement de nos processeurs modernes.

4voto

O_Z Points 1400

Suppression de la ligne s += A[curIndex+1] + A[size-curIndex-1]; permet à optimisation récursive de la queue . Cette optimisation ne peut se faire que si l'appel récursif se trouve dans la dernière ligne de la fonction.

https://en.wikipedia.org/wiki/Tail_call

3voto

Harald Points 2247

Il est intéressant de noter que la première exécution comporte environ 30 % de branches en plus que la seconde (32 millions de branches contre 24 millions).

J'ai généré le code d'assemblage pour votre application en utilisant gcc 4.8.5 et les mêmes drapeaux (plus -S ) et il existe une différence significative entre les assemblages. Le code avec la déclaration conflictuelle compte environ 572 lignes, tandis que le code sans la même déclaration ne compte que 409 lignes. En se concentrant sur le symbole _Z5test1i -- le nom C++ décoré pour test1), la routine est longue de 367 lignes alors que le second cas n'occupe que 202 lignes. De toutes ces lignes, le premier cas contient 36 branches (plus 15 instructions d'appel) et le second cas contient 34 branches (plus 1 instruction d'appel).

Il est également intéressant de constater que la compilation de l'application avec -O1 n'expose pas cette divergence entre les deux versions (bien que la mauvaise prédiction de la branche soit plus élevée, environ 12%). En utilisant -O2 montre une différence entre les deux versions (12% vs 3% de mauvaises prédictions de branche).

Je ne suis pas un expert en compilation pour comprendre les flux de contrôle et les logiques utilisées par le compilateur, mais il semble que le compilateur soit capable de réaliser des optimisations plus intelligentes (y compris peut-être des optimisations récursives de queue comme indiqué par l'utilisateur1850903 dans sa réponse) lorsque cette partie du code n'est pas présente.

3voto

Bruno Ferreira Points 1352

Le problème est le suivant :

if(A[curIndex] == 1) return;

chaque appel de la fonction de test alterne le résultat de cette comparaison, en raison de certaines optimisations, puisque le tableau est par exemple 0,0,0,0,0,1,1,1,1

En d'autres termes :

  1. curIndex = 0 -> A[0] = 0
  2. test1(curIndex + 1) -> curIndex = 1 -> A[1] = 0

Mais alors, l'architecture du processeur PEUT-ÊTRE (une grande possibilité, car cela dépend ; pour moi, cette optimisation est désactivée - un i5-6400) ont une fonction appelée tête de lecture (effectué le long de la prédiction de branchement), qui exécute les instructions restantes dans le pipeline avant d'effectuer un branchement ; il exécutera donc test1(size - curIndex -1) avant l'instruction if incriminée.

En supprimant l'attribution, on entre dans une autre optimisation, comme l'a dit l'utilisateur 1850903.

3voto

kfsone Points 7375

Le morceau de code suivant est récursif en queue : la dernière ligne de la fonction ne nécessite pas d'appel, mais simplement un branchement au point où la fonction commence à utiliser le premier argument :

void f(int i) {
    if (i == size) break;
    s += a[i];
    f(i + 1);
}

Cependant, si nous cassons cela et le rendons récursif sans queue :

void f(int i) {
    if (i == size) break;
    f(i + 1);
    s += a[i];
}

Il existe un certain nombre de raisons pour lesquelles le compilateur ne peut pas déduire que cette dernière est récursive en queue, mais dans l'exemple que vous avez donné,

test(A[N]);
test(A[M]);
s += a[N] + a[M];

les mêmes règles s'appliquent. Le compilateur ne peut pas déterminer qu'il s'agit d'une queue récursive, mais surtout il ne peut pas le faire à cause des deux appels (voir avant),filterAsm:(colouriseAsm:!t,commentOnly:!t,directives:!t,labels:!t),version:3) y après),filterAsm:(colouriseAsm:!t,commentOnly:!t,directives:!t,labels:!t),version:3) ).

Ce que vous semblez attendre du compilateur est une fonction qui effectue quelques branchements conditionnels simples, deux appels et quelques chargements/additions/stockages.

Au lieu de cela, le compilateur déroule cette boucle et génère du code qui a beaucoup de points de branchement. Ceci est fait en partie parce que le compilateur pense qu'il sera plus efficace de cette façon (impliquant moins ) mais en partie parce qu'il diminue la profondeur de récursion à l'exécution.

int size;
int* A;
int half;
int s;

void test1(int curIndex){
  if(curIndex == half || A[curIndex] == 1) return;
  test1(curIndex+1);
  test1(size-curIndex-1);
  s += A[curIndex+1] + A[size-curIndex-1];
}

produit :

test1(int):
        movl    half(%rip), %edx
        cmpl    %edi, %edx
        je      .L36
        pushq   %r15
        pushq   %r14
        movslq  %edi, %rcx
        pushq   %r13
        pushq   %r12
        leaq    0(,%rcx,4), %r12
        pushq   %rbp
        pushq   %rbx
        subq    $24, %rsp
        movq    A(%rip), %rax
        cmpl    $1, (%rax,%rcx,4)
        je      .L1
        leal    1(%rdi), %r13d
        movl    %edi, %ebp
        cmpl    %r13d, %edx
        je      .L42
        cmpl    $1, 4(%rax,%r12)
        je      .L42
        leal    2(%rdi), %ebx
        cmpl    %ebx, %edx
        je      .L39
        cmpl    $1, 8(%rax,%r12)
        je      .L39
        leal    3(%rdi), %r14d
        cmpl    %r14d, %edx
        je      .L37
        cmpl    $1, 12(%rax,%r12)
        je      .L37
        leal    4(%rdi), %edi
        call    test1(int)
        movl    %r14d, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rax
        movl    %ecx, %esi
        movl    16(%rax,%r12), %edx
        subl    %r14d, %esi
        movslq  %esi, %rsi
        addl    -4(%rax,%rsi,4), %edx
        addl    %edx, s(%rip)
        movl    half(%rip), %edx
.L10:
        movl    %ecx, %edi
        subl    %ebx, %edi
        leal    -1(%rdi), %r14d
        cmpl    %edx, %r14d
        je      .L38
        movslq  %r14d, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r15
        je      .L38
        call    test1(int)
        movl    %r14d, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rax
        movl    %ecx, %edx
        movl    4(%rax,%r15), %esi
        movl    %ecx, %edi
        subl    %r14d, %edx
        subl    %ebx, %edi
        movslq  %edx, %rdx
        addl    -4(%rax,%rdx,4), %esi
        movl    half(%rip), %edx
        addl    s(%rip), %esi
        movl    %esi, s(%rip)
.L13:
        movslq  %edi, %rdi
        movl    12(%rax,%r12), %r8d
        addl    -4(%rax,%rdi,4), %r8d
        addl    %r8d, %esi
        movl    %esi, s(%rip)
.L7:
        movl    %ecx, %ebx
        subl    %r13d, %ebx
        leal    -1(%rbx), %r14d
        cmpl    %edx, %r14d
        je      .L41
        movslq  %r14d, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r15
        je      .L41
        cmpl    %edx, %ebx
        je      .L18
        movslq  %ebx, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r8
        movq    %r8, (%rsp)
        je      .L18
        leal    1(%rbx), %edi
        call    test1(int)
        movl    %ebx, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rax
        movq    (%rsp), %r8
        movl    %ecx, %esi
        subl    %ebx, %esi
        movl    4(%rax,%r8), %edx
        movslq  %esi, %rsi
        addl    -4(%rax,%rsi,4), %edx
        addl    %edx, s(%rip)
        movl    half(%rip), %edx
.L18:
        movl    %ecx, %edi
        subl    %r14d, %edi
        leal    -1(%rdi), %ebx
        cmpl    %edx, %ebx
        je      .L40
        movslq  %ebx, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r8
        je      .L40
        movq    %r8, (%rsp)
        call    test1(int)
        movl    %ebx, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rax
        movq    (%rsp), %r8
        movl    %ecx, %edx
        movl    %ecx, %edi
        subl    %ebx, %edx
        movl    4(%rax,%r8), %esi
        subl    %r14d, %edi
        movslq  %edx, %rdx
        addl    -4(%rax,%rdx,4), %esi
        movl    half(%rip), %edx
        addl    s(%rip), %esi
        movl    %esi, %r8d
        movl    %esi, s(%rip)
.L20:
        movslq  %edi, %rdi
        movl    4(%rax,%r15), %esi
        movl    %ecx, %ebx
        addl    -4(%rax,%rdi,4), %esi
        subl    %r13d, %ebx
        addl    %r8d, %esi
        movl    %esi, s(%rip)
.L16:
        movslq  %ebx, %rbx
        movl    8(%rax,%r12), %edi
        addl    -4(%rax,%rbx,4), %edi
        addl    %edi, %esi
        movl    %esi, s(%rip)
        jmp     .L4
.L45:
        movl    s(%rip), %edx
.L23:
        movslq  %ebx, %rbx
        movl    4(%rax,%r12), %ecx
        addl    -4(%rax,%rbx,4), %ecx
        addl    %ecx, %edx
        movl    %edx, s(%rip)
.L1:
        addq    $24, %rsp
        popq    %rbx
        popq    %rbp
        popq    %r12
        popq    %r13
        popq    %r14
        popq    %r15
.L36:
        rep ret
.L42:
        movl    size(%rip), %ecx
.L4:
        movl    %ecx, %ebx
        subl    %ebp, %ebx
        leal    -1(%rbx), %r14d
        cmpl    %edx, %r14d
        je      .L45
        movslq  %r14d, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r15
        je      .L45
        cmpl    %edx, %ebx
        je      .L25
        movslq  %ebx, %rsi
        cmpl    $1, (%rax,%rsi,4)
        leaq    0(,%rsi,4), %r13
        je      .L25
        leal    1(%rbx), %esi
        cmpl    %edx, %esi
        movl    %esi, (%rsp)
        je      .L26
        cmpl    $1, 8(%rax,%r15)
        je      .L26
        leal    2(%rbx), %edi
        call    test1(int)
        movl    (%rsp), %esi
        movl    %esi, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movl    (%rsp), %esi
        movq    A(%rip), %rax
        movl    %ecx, %edx
        subl    %esi, %edx
        movslq  %edx, %rsi
        movl    12(%rax,%r15), %edx
        addl    -4(%rax,%rsi,4), %edx
        addl    %edx, s(%rip)
        movl    half(%rip), %edx
.L26:
        movl    %ecx, %edi
        subl    %ebx, %edi
        leal    -1(%rdi), %esi
        cmpl    %edx, %esi
        je      .L43
        movslq  %esi, %r8
        cmpl    $1, (%rax,%r8,4)
        leaq    0(,%r8,4), %r9
        je      .L43
        movq    %r9, 8(%rsp)
        movl    %esi, (%rsp)
        call    test1(int)
        movl    (%rsp), %esi
        movl    %esi, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movl    (%rsp), %esi
        movq    A(%rip), %rax
        movq    8(%rsp), %r9
        movl    %ecx, %edx
        movl    %ecx, %edi
        subl    %esi, %edx
        movl    4(%rax,%r9), %esi
        subl    %ebx, %edi
        movslq  %edx, %rdx
        addl    -4(%rax,%rdx,4), %esi
        movl    half(%rip), %edx
        addl    s(%rip), %esi
        movl    %esi, s(%rip)
.L28:
        movslq  %edi, %rdi
        movl    4(%rax,%r13), %r8d
        addl    -4(%rax,%rdi,4), %r8d
        addl    %r8d, %esi
        movl    %esi, s(%rip)
.L25:
        movl    %ecx, %r13d
        subl    %r14d, %r13d
        leal    -1(%r13), %ebx
        cmpl    %edx, %ebx
        je      .L44
        movslq  %ebx, %rdi
        cmpl    $1, (%rax,%rdi,4)
        leaq    0(,%rdi,4), %rsi
        movq    %rsi, (%rsp)
        je      .L44
        cmpl    %edx, %r13d
        je      .L33
        movslq  %r13d, %rdx
        cmpl    $1, (%rax,%rdx,4)
        leaq    0(,%rdx,4), %r8
        movq    %r8, 8(%rsp)
        je      .L33
        leal    1(%r13), %edi
        call    test1(int)
        movl    %r13d, %edi
        notl    %edi
        addl    size(%rip), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rdi
        movq    8(%rsp), %r8
        movl    %ecx, %edx
        subl    %r13d, %edx
        movl    4(%rdi,%r8), %eax
        movslq  %edx, %rdx
        addl    -4(%rdi,%rdx,4), %eax
        addl    %eax, s(%rip)
.L33:
        subl    %ebx, %ecx
        leal    -1(%rcx), %edi
        call    test1(int)
        movl    size(%rip), %ecx
        movq    A(%rip), %rax
        movl    %ecx, %esi
        movl    %ecx, %r13d
        subl    %ebx, %esi
        movq    (%rsp), %rbx
        subl    %r14d, %r13d
        movslq  %esi, %rsi
        movl    4(%rax,%rbx), %edx
        addl    -4(%rax,%rsi,4), %edx
        movl    s(%rip), %esi
        addl    %edx, %esi
        movl    %esi, s(%rip)
.L31:
        movslq  %r13d, %r13
        movl    4(%rax,%r15), %edx
        subl    %ebp, %ecx
        addl    -4(%rax,%r13,4), %edx
        movl    %ecx, %ebx
        addl    %esi, %edx
        movl    %edx, s(%rip)
        jmp     .L23
.L44:
        movl    s(%rip), %esi
        jmp     .L31
.L39:
        movl    size(%rip), %ecx
        jmp     .L7
.L41:
        movl    s(%rip), %esi
        jmp     .L16
.L43:
        movl    s(%rip), %esi
        jmp     .L28
.L38:
        movl    s(%rip), %esi
        jmp     .L13
.L37:
        movl    size(%rip), %ecx
        jmp     .L10
.L40:
        movl    s(%rip), %r8d
        jmp     .L20
s:
half:
        .zero   4
A:
        .zero   8
size:
        .zero   4

Pour le cas des valeurs alternées, en supposant que la taille == 7 :

test1(curIndex = 0)
{
    if (curIndex == size - 1) return;  // false x1
    if (A[curIndex] == 1) return;  // false x1

    test1(curIndex + 1 => 1) {
        if (curIndex == size - 1) return;  // false x2
        if (A[curIndex] == 1) return;  // false x1 -mispred-> returns
    }

    test1(curIndex + 2 => 2) {
        if (curIndex == size - 1) return; // false x 3
        if (A[curIndex] == 1) return;  // false x2
        test1(curIndex + 1 => 3) {
            if (curIndex == size - 1) return;  // false x3
            if (A[curIndex] == 1) return;  // false x2 -mispred-> returns
        }
        test1(curIndex + 2 => 4) {
            if (curIndex == size - 1) return;  // false x4
            if (A[curIndex] == 1) return; // false x3
            test1(curIndex + 1 => 5) {
                if (curIndex == size - 1) return; // false x5
                if (A[curIndex] == 1) return; // false x3 -mispred-> returns
            }
            test1(curIndex + 2 => 6) {
                if (curIndex == size - 1) return; // false x5 -mispred-> returns
            }
            s += A[5] + A[6];
        }
        s += A[3] + A[4];
    }
    s += A[1] + A[2];
}

Et imaginons un cas où

size = 11;
A[11] = { 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0 };

test1(0)
  -> test1(1)
       -> test1(2)
            -> test1(3)  -> returns because 1
            -> test1(4)
                 -> test1(5)
                      -> test1(6)
                           -> test1(7) -- returns because 1
                           -> test1(8)
                                -> test1(9) -- returns because 1
                                -> test1(10) -- returns because size-1
                      -> test1(7) -- returns because 1
                 -> test1(6)
                   -> test1(7)
                   -> test1(8)
                        -> test1(9) -- 1
                        -> test1(10) -- size-1
       -> test1(3)  -> returns
  -> test1(2)
       ... as above

o

size = 5;
A[5] = { 0, 0, 0, 0, 1 };

test1(0)
  -> test1(1)
       -> test1(2)
            -> test1(3)
                 -> test1(4)  --  size
                 -> test1(5)  --  UB
            -> test1(4)
       -> test1(3)
            -> test1(4)  -- size
            -> test1(5)  -- UB
  -> test1(2)
       ..

Les deux cas que vous avez distingués (alternance et demi-modèle) sont des extrêmes optimaux et le compilateur a choisi un cas intermédiaire qu'il essaiera de traiter au mieux.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X