Подтвердить что ты не робот

Почему эта функция С++ выдает так много неверных прогнозов отрасли?

Пусть A - массив, содержащий нечетное число нулей и единиц. Если n - размер A, то A построен таким образом, что первые ceil(n/2) элементы 0, а остальные элементы 1.

Итак, если n = 9, A будет выглядеть так:

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

Цель состоит в том, чтобы найти сумму 1s в массиве, и мы делаем это, используя эту функцию:

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];

}

Эта функция довольно глупо для заданной проблемы, но это симуляция другой функции, которую я хочу выглядеть так и производит такое же количество неверных предсказаний.

Вот весь код эксперимента:

#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;
}

Скомпилируйте, набрав g++ -O3 -std=c++11 file.cpp и запустите, набрав ./executable size{odd integer}.

Я использую процессор Intel Core i5-3470 с частотой 8 ГБ, 8 ГБ оперативной памяти, кеш L1 256 КБ, кеш второго уровня 1 МБ, кеш-память L3 6 МБ.

Запуск perf stat -B -e branches,branch-misses ./cachetests 111111 дает мне следующее:

   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

если я удалю строку

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

Я получаю следующий вывод от 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

Что эта линия должна делать с предсказаниями ветвей, когда она даже не является выражением if?

Как я вижу это в первых вызовах ceil(n/2) - 1 test1(), оба оператора if будут ложными. В вызове ceil(n/2)-th значение if(curIndex == ceil(n/2)) будет истинным. В остальных вызовах n-ceil(n/2) первый оператор будет ложным, а второй оператор будет истинным.

Почему Intel не может предсказать такое простое поведение?

Теперь посмотрим на второй случай. Предположим, что A теперь имеет чередующиеся нули и единицы. Мы всегда будем начинать с 0. Поэтому, если n = 9 A будет выглядеть так:

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

Функция, которую мы будем использовать, следующая:

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];

}

И вот весь код эксперимента:

#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;
}

Я запускаю perf, используя те же команды, что и раньше:

    Performance counter stats for './cachetests2 111111':

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

   0.037134196 seconds time elapsed

И удаление этой строки еще немного улучшило ситуацию:

   Performance counter stats for './cachetests2 111111':

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

   0.009977772 seconds time elapsed

Теперь, если мы проанализируем функцию, if(curIndex == size-1) будет false n-1 раз, а if(A[curIndex] == 1) будет чередоваться от true к false.

Как я вижу, обе функции должны быть легко предсказать, однако это не относится к первой функции. В то же время я не уверен, что происходит с этой линией и почему она играет роль в улучшении поведения ветвей.

4b9b3361

Ответ 1

Вот мои мысли об этом, глядя на него некоторое время. Прежде всего, проблема легко воспроизводится с помощью -O2, поэтому лучше использовать это как ссылка, поскольку он генерирует простой нерасширенный код, который легко анализировать. Проблема с -O3 по существу одинакова, она немного менее очевидна.

Итак, для первого случая (полу-нули с полуподобным рисунком) компилятор генерирует этот код:

 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 

Очень просто, что бы вы ожидали - две условные ветки, две звонки. Он дает нам эту (или аналогичную) статистику по Core 2 Duo T6570, AMD Phenom II X4 925 и 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

Если вы хотите внести это изменение, переместите назначение перед рекурсивными вызовами:

 --- 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];

  }

Изображение меняется:

 $ 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

И да, как уже отмечалось, он напрямую связан с хвостовой рекурсией оптимизация, потому что, если вы хотите скомпилировать исправленный код с помощью -fno-optimize-sibling-calls вы получите те же "плохие" результаты. Итак, начнем посмотрите, что у нас есть в сборке с оптимизацией хвостового вызова:

 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)

Он имеет четыре условные ветки с одним вызовом. Поэтому проанализируйте данные мы до сих пор.

Прежде всего, что такое ветвящаяся инструкция с точки зрения процессора? Это любой из call, ret, j* (включая прямой jmp) и loop. call и jmp немного неинтуитивны, но они имеют решающее значение для правильного подсчета.

В целом, мы ожидаем, что эту функцию будем называть 11111100 раз, по одному для каждого элемент, что примерно 11M. В версии с оптимизированной не-хвостом мы видим 45M, инициализация в main() всего 111K, все остальные вещи незначительны, поэтому основной вклад в это число приходит наша функция. Наша функция call -ed, она вычисляет первый je, который является истинным во всех случаях, кроме одного, затем он вычисляет второй je, который является истинной половиной времени, а затем он либо называет себя рекурсивно ( но мы уже подсчитали, что функция вызывается 11M раз) или возвращает (как и после рекурсивных вызовов). Так что 4 команды ветвления на 11M-вызовы, именно то число, которое мы видим. Из них около 5.5M-ветвей пропущено, что предполагает, что эти промахи проистекают из одной неверно предсказанной инструкции, либо то, что оценивалось 11M раз, так и пропущено примерно в 50% случаев или что-то, что оценивалось в течение половины времени и всегда упускалось.

Что у нас есть в версии с оптимизацией хвоста? У нас есть функция, называемая около 5,5 М раз, но теперь каждый вызов вызывает один call, изначально две ветки (первая из них истинна во всех случаях, кроме одного, а вторая всегда ложна из-за наших данных), затем jmp, затем вызов (но мы уже подсчитали, что у нас есть вызовы 5.5M), затем ветвь в 400ae8 и ветка в 400ab6 (всегда истинная из-за наших данных), а затем возвращаемся. Таким образом, в среднем, что четыре условных ветки, один безусловный прыжок, вызов и одна непрямая ветвь (возврат от функции), 5,5 М раз 7, дают нам общий подсчет около 39М ветвей, как мы видим в первичном выходе.

Что мы знаем, так это то, что у процессора нет никаких проблем при прогнозировании событий в потоке с помощью одного вызова функции (хотя эта версия имеет более условные ветки), и у нее есть проблемы с двумя вызовами функций. Таким образом, это говорит о том, что проблема заключается в возвратах функции.

К сожалению, мы очень мало знаем о деталях того, как именно отрасль предикторов наших современных процессоров. Лучший анализ, который я смог найти это, и это говорит о том, что у процессоров есть буфер стека возврата около 16 записей. Если мы снова вернемся к нашим данным с этим находкой, все начнет немного разъяснять.

Когда у вас есть пол-нули с половинным рисунком, вы рекурсивно глубоко в test1(curIndex+1), но затем вы начинаете возвращаться назад и вызов test1(size-curIndex-1). Эта рекурсия никогда не бывает глубже, чем одна вызовом, поэтому для него предсказаны результаты. Но помните, что мы теперь 55555 вызовов в глубину, и процессор помнит последние 16, так что это не удивительно, что он не может догадаться о наших возвращениях, начиная с глубины 55539 уровня, более удивительно, что он может сделать это с помощью оптимизированной с помощью хвоста версии.

Собственно, поведение версии, оптимизированной для хвоста, предполагает, что отсутствует любая другая информация о возврате, процессор просто предполагает, что право один из них - последний. Это также подтверждается поведением версия, оптимизированная для не-хвостов, поскольку она имеет 55555 вызовов вглубь test1(curIndex+1), а затем по возвращении он всегда получает один уровень вглубь test1(size-curIndex-1), поэтому, когда мы находимся от 55555 глубины до 55539 глубины (или независимо от вашего буфера возврата процессора), он вызывает test1(size-curIndex-1), возвращается от этого и не имеет абсолютно никакого информацию о следующем возврате, поэтому он предполагает, что мы должны вернуться к последний увиденный адрес (который является адресом для возврата к test1(size-curIndex-1)), и это, очевидно, неверно. 55539 раз неправильно. С 100 циклов функции, что в точности предсказание ветвления 5.5M мы видим.

Теперь давайте перейдем к вашему альтернативному шаблону и коду для этого. Этот код на самом деле очень разные, если вы хотите проанализировать, как это происходит в глубина. Здесь у вас есть test2(curIndex+1), который всегда возвращается немедленно и ваш test2(curIndex+2), чтобы всегда идти глубже. Таким образом, доходы от test2(curIndex+1) всегда предсказаны отлично (они просто не углубляются достаточно), и когда мы закончим нашу рекурсию в test2(curIndex+2), это всегда возвращается в ту же точку, все 55555 раз, поэтому процессор не имеет проблемы с этим.

Это может быть подтверждено этим небольшим изменением на ваши исходные полуоси с кодом полужидов:

--- 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];

Итак, теперь созданный код по-прежнему не оптимизирован для хвостового вызова (по-своему он очень похож на оригинал), но вы получаете что-то вроде этого в первичном выходе:

$ 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

Как и ожидалось, теперь наш первый вызов всегда возвращается немедленно, а второй - 55555, а затем возвращается только в ту же точку.

Теперь, когда это решит, позвольте мне показать что-то в моем рукаве. В одной системе и то есть Core i5-5200U, оригинальные полунуки с оптимизацией без хвоста с половинной версией показывают следующие результаты:

 $ 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

Итак, по-видимому, Бродвелл легко справляется с этим шаблоном, что возвращает нас к вопрос о том, насколько мы знаем о логике прогнозирования ветвей нашего современных процессоров.

Ответ 2

Удаление строки s += A[curIndex+1] + A[size-curIndex-1]; позволяет оптимизировать хвостовую рекурсию. Эта оптимизация может произойти только тогда, когда рекурсивный вызов находится в последней строке функции.

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

Ответ 3

Интересно, что в первом исполнении у вас на 30% больше ветвей, чем во втором исполнении (32M ветки против 24 Mbranches).

Я создал код сборки для вашего приложения, используя gcc 4.8.5 и те же флаги (плюс -S), и существует значительная разница между сборками. Код с конфликтующим утверждением составляет около 572 строк, а код без одного и того же оператора - всего 409 строк. Ориентируясь на символ _Z5test1i - оформленное имя С++ для test1), подпрограмма длится 367 строк, а второй случай занимает всего 202 строки. Из всех этих строк первый случай содержит 36 ветвей (плюс 15 инструкций вызова), а второй случай содержит 34 ответвления (плюс 1 команда вызова).

Интересно также, что компиляция приложения с помощью -O1 не подвергает этому расхождению между двумя версиями (хотя неверный прогноз ветки выше, около 12%). Использование -O2 показывает разницу между двумя версиями (12% против 3% от неверных прогнозов ветвления).

Я не специалист по компилятору, чтобы понять потоки управления и логики, используемые компилятором, но похоже, что компилятор способен добиться более разумных оптимизаций (возможно, включая хвостовые рекурсивные оптимизации, как указано пользователем1850903 в своем ответе), когда это часть кода отсутствует.

Ответ 4

проблема такова:

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

каждый вызов тестовой функции чередует результат этого сравнения из-за некоторых оптимизаций, так как массив является, например, 0,0,0,0,0,1,1,1,1

Другими словами:

  • curIndex = 0 → A [0] = 0
  • test1 (curIndex + 1) → curIndex = 1 → A [1] = 0

Но тогда процессорная архитектура МОЖЕТ (это может сильно повлиять, потому что для меня эта оптимизация отключена - i5-6400) имеют функцию runahead (выполняемую по предсказанию ветвления) который выполняет остальные инструкции в конвейере перед входом в ветвь; поэтому он выполнит test1(size - curIndex -1) перед инструкцией о нарушении.

При удалении атрибуции, он вводит другую оптимизацию, как сказал пользователь1850903.

Ответ 5

Следующий фрагмент кода является хвостовым рекурсивным: последняя строка функции не требует вызова, просто ветвь до точки, где функция начинает использовать первый аргумент:

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

Однако, если мы сломаем это и сделаем его не-хвостом рекурсивным:

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

Существует несколько причин, по которым компилятор не может вывести последний как хвост-рекурсивный, но в примере, который вы указали,

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

действуют те же правила. Компилятор не может определить, что это хвост рекурсивный, но тем более он не может этого сделать из-за двух вызовов (см. до и после).

То, что вы ожидаете от компилятора, - это функция, которая выполняет пару простых условных ветвей, два вызова и некоторые загрузки/добавления/хранилища.

Вместо этого компилятор разворачивает этот цикл и генерирует код, который имеет много точек ветвления. Это делается частично потому, что компилятор считает, что он будет более эффективным таким образом (с меньшим количеством ветвей), но отчасти потому, что он уменьшает глубину рекурсии во время выполнения.

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];
}

дает:

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

Для случая переменных значений, предполагая размер == 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];
}

И давайте представим случай, когда

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

или

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)
       ..

Два случая, которые вы выделили (чередующиеся и полу-шаблоны), являются оптимальными крайними значениями, а компилятор выбрал промежуточный пример, который он попытается обработать наилучшим образом.