variable tipos significado programacion ejemplos ejemplo declaracion c++ performance branch-prediction

tipos - ¿Por qué esta función de C++ produce tantas fallas de predicción de ramas?



ejemplo de variable local en c++ (5)

Aquí están mis pensamientos sobre esto después de mirarlo por un tiempo. En primer lugar, el problema es fácilmente reproducible con -O2 , por lo que es mejor utilizarlo como referencia, ya que genera un código simple no desenrollado que es fácil de analizar. El problema con -O3 es esencialmente el mismo, es un poco menos obvio.

Entonces, para el primer caso (medio ceros con un patrón de medio uno) el compilador genera este código:

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

Muy simple, tipo de lo que cabría esperar: dos ramas condicionales, dos llamadas. Nos proporciona esta (o similar) estadística sobre Core 2 Duo T6570, AMD Phenom II X4 925 y 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 quiere hacer este cambio, mueva la tarea antes de las llamadas recursivas:

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

La imagen cambia:

$ 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

Y sí, como ya se señaló, está directamente relacionado con la optimización de la recursividad de cola, porque si compila el código parcheado con -fno-optimize-sibling-calls obtendrá los mismos resultados "malos". Entonces, veamos qué tenemos en ensamble con la optimización de la cola de llamadas:

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)

Tiene cuatro ramas condicionales con una llamada. Analicemos la información que tenemos hasta ahora.

Antes que nada, ¿qué es una instrucción de ramificación desde la perspectiva del procesador? Es cualquiera de call , ret , j* (incluido jmp directo) y loop . call y jmp son un poco intuitivos, pero son cruciales para contar las cosas correctamente.

En general, esperamos que esta función se llame 11111100 veces, una para cada elemento, que es aproximadamente 11M. En la versión optimizada para llamadas sin cola, vemos aproximadamente 45M ramas, la inicialización en main () es solo 111K, todas las otras cosas son menores, por lo que la principal contribución a este número proviene de nuestra función. Nuestra función es call -ed, evalúa la primera je , que es verdadera en todos los casos menos en uno, luego evalúa la segunda je , que es verdadera la mitad de las veces y luego se llama recursivamente (pero ya hemos contado que la función se invoca 11M veces) o regresa (como lo hace después de llamadas recursivas. Así que son 4 instrucciones de bifurcación por llamadas 11M, exactamente el número que vemos. Fuera de estas alrededor de 5,5M se pierden ramas, lo que sugiere que todas estas fallas vienen de una instrucción mal predicha, ya sea algo que se evalúa 11 millones de veces y se pierde alrededor del 50% del tiempo o algo que se evalúa la mitad del tiempo y se pierde siempre.

¿Qué tenemos en la versión de cola optimizada? Tenemos la función llamada alrededor de 5,5M veces, pero ahora cada invocación incurre en una call , dos ramas inicialmente (la primera es verdadera en todos los casos, excepto uno y la segunda siempre es falsa debido a nuestros datos), luego una jmp , luego una llamada (pero ya hemos contado que tenemos llamadas de 5.5M), luego una rama a 400ae8 y una rama a 400ab6 (siempre es verdad debido a nuestros datos), luego regrese. Entonces, en promedio, son cuatro ramas condicionales, un salto incondicional, una llamada y una rama indirecta (retorno de la función), 5.5M por 7 nos da un recuento total de alrededor de 39M de ramas, exactamente como lo vemos en la salida de perf.

Lo que sabemos es que el procesador no tiene ningún problema en predecir cosas en un flujo con una llamada de función (aunque esta versión tiene más ramas condicionales) y tiene problemas con dos llamadas de función. Entonces sugiere que el problema está en los retornos de la función.

Desafortunadamente, sabemos muy poco sobre los detalles de cómo funcionan exactamente los predictores de bifurcación de nuestros procesadores modernos. El mejor análisis que pude encontrar es este y sugiere que los procesadores tienen un buffer de pila de retorno de alrededor de 16 entradas. Si volvemos a nuestros datos nuevamente con este hallazgo, las cosas empiezan a aclararse un poco.

Cuando tiene medio ceros con patrón de la mitad uno, está recurriendo profundamente en test1(curIndex+1) , pero luego comienza a regresar y llama a test1(size-curIndex-1) . Esa recursión nunca es más profunda que una llamada, por lo que los retornos se predicen perfectamente. Pero recuerde que ahora tenemos 55555 invocaciones profundas y el procesador solo recuerda los últimos 16, por lo que no es sorprendente que no pueda adivinar nuestros retornos a partir de un nivel 55539 profundo, es más sorprendente que pueda hacerlo con tail-call- versión optimizada

En realidad, el comportamiento de la versión optimizada de la llamada de la cola sugiere que, al omitir otra información sobre los retornos, el procesador simplemente asume que el correcto es el último visto. También está probado por el comportamiento de la versión optimizada para llamadas sin cola, ya que entra 55555 llamadas en el test1(curIndex+1) y luego, al regresar, siempre tiene un nivel de profundidad en test1(size-curIndex-1) , así que cuando pasamos de 55555-deep a 55539-deep (o lo que sea que sea el buffer de retorno de su procesador) llama a test1(size-curIndex-1) , regresa de eso y no tiene absolutamente ninguna información sobre el próximo retorno, por lo se supone que debemos volver a la última dirección que se ve (que es la dirección a la que regresar desde test1(size-curIndex-1) ) y obviamente está mal. 55539 veces mal. Con 100 ciclos de la función, eso es exactamente lo que falla en la predicción de bifurcación de 5.5M.

Ahora veamos tu patrón alternativo y el código para eso. Este código es realmente muy diferente, si quieres analizar cómo se profundiza. Aquí tiene su test2(curIndex+1) siempre regresa inmediatamente y su test2(curIndex+2) para ir siempre más profundo. Así que los retornos de test2(curIndex+1) siempre se predicen perfectamente (simplemente no profundizan lo suficiente) y cuando vamos a terminar nuestra recursión en test2(curIndex+2) , siempre regresa al mismo punto, todos 55555 veces, por lo que el procesador no tiene problemas con eso.

Esto puede demostrarse aún más con este pequeño cambio en sus semicírgenes originales con código de la mitad:

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

Así que ahora el código generado aún no está optimizado para la cola de llamada (en lo que respecta al ensamblaje, es muy similar al original), pero se obtiene algo como esto en la salida 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

Como era de esperar, ahora nuestra primera llamada siempre regresa inmediatamente y la segunda llamada tiene 55555 de profundidad y luego solo regresa al mismo punto.

Ahora con eso resuelto déjame mostrar algo en mi manga. En un sistema, y ​​es Core i5-5200U, los medios ceros originales sin cola optimizados con versiones de mitad de uno muestran los siguientes resultados:

$ 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

Entonces, aparentemente, Broadwell puede manejar este patrón fácilmente, lo que nos devuelve a la pregunta de cuánto sabemos sobre la lógica de predicción de bifurcación de nuestros procesadores modernos.

Deje A ser una matriz que contiene un número impar de ceros y unos. Si n es el tamaño de A , entonces A está construido de tal manera que los primeros elementos ceil(n/2) son 0 y los elementos restantes 1 .

Entonces, si n = 9 , A se vería así:

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

El objetivo es encontrar la suma de 1s en la matriz y lo hacemos usando esta función:

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

Esta función es bastante tonta para el problema dado, pero es una simulación de una función diferente que quiero que se vea así y está produciendo la misma cantidad de errores de predicción de ramas.

Aquí está el código completo del experimento:

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

Para compilar, escriba g++ -O3 -std=c++11 file.cpp y ejecute escribiendo ./executable size{odd integer} .

Estoy usando una CPU Intel (R) Core (TM) i5-3470 @ 3.20GHz con 8 GB de RAM, caché L1 de 256 KB, caché L2 de 1 MB, caché L3 de 6 MB.

Ejecutar perf stat -B -e branches,branch-misses ./cachetests 111111 me da lo siguiente:

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 elimino la línea

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

Obtengo el siguiente resultado 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é tiene que ver esa línea con las predicciones de ramas cuando ni siquiera es una declaración if?

La forma en que lo veo, en el primer ceil(n/2) - 1 llamadas de test1() , ambas si las declaraciones serán falsas. En la llamada if(curIndex == ceil(n/2)) , if(curIndex == ceil(n/2)) será verdadero. En las llamadas n-ceil(n/2) restantes, la primera instrucción será falsa y la segunda será verdadera.

¿Por qué Intel no puede predecir un comportamiento tan simple?

Ahora veamos un segundo caso. Supongamos que A ahora tiene ceros y unos alternativos. Siempre comenzaremos desde 0. Entonces, si n = 9 A se verá así:

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

La función que vamos a usar es la siguiente:

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

Y aquí está el código completo del experimento:

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

Ejecuto perf utilizando los mismos comandos que antes:

Performance counter stats for ''./cachetests2 111111'': 28,560,183 branches 54,204 branch-misses # 0.19% of all branches 0.037134196 seconds time elapsed

Y eliminar esa línea nuevamente mejoró un poco las cosas:

Performance counter stats for ''./cachetests2 111111'': 28,419,557 branches 16,636 branch-misses # 0.06% of all branches 0.009977772 seconds time elapsed

Ahora si analizamos la función, if(curIndex == size-1) será falso n-1 veces, y if(A[curIndex] == 1) se alternará de verdadero a falso.

Tal como lo veo, ambas funciones deberían ser fáciles de predecir, sin embargo, este no es el caso para la primera función. Al mismo tiempo, no estoy seguro de qué está pasando con esa línea y por qué desempeña un papel en la mejora del comportamiento de las sucursales.


Curiosamente, en la primera ejecución tiene aproximadamente un 30% más de sucursales que en la segunda ejecución (32 millones de sucursales frente a 24 millones de marcos).

He generado el código de ensamblaje para su aplicación usando gcc 4.8.5 y las mismas banderas (más -S ) y hay una diferencia significativa entre los ensamblajes. El código con la declaración en conflicto es de aproximadamente 572 líneas, mientras que el código sin la misma declaración es solo 409 líneas. Centrándonos en el símbolo _Z5test1i - el nombre decorado de C ++ para test1), la rutina tiene 367 líneas de longitud, mientras que el segundo caso ocupa solo 202 líneas. De todas esas líneas, el primer caso contiene 36 ramas (más 15 instrucciones de llamada) y el segundo caso contiene 34 ramas (más 1 instrucción de llamada).

También es interesante que la compilación de la aplicación con -O1 no expone esta divergencia entre las dos versiones (aunque el error de la bifurcación es mayor, aproximadamente 12%). El uso de -O2 muestra una diferencia entre las dos versiones (12% frente a 3% de -O2 de la bifurcación).

No soy un experto en compiladores para comprender los flujos de control y las lógicas utilizadas por el compilador, pero parece que el compilador puede lograr optimizaciones más inteligentes (tal vez incluyendo optimizaciones recursivas de cola según lo señala user1850903 en su respuesta) cuando esa parte del el código no está presente


El siguiente fragmento de código es recursivo de cola: la última línea de la función no requiere una llamada, simplemente una rama al punto donde la función comienza utilizando el primer argumento:

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

Sin embargo, si rompemos esto y lo hacemos no recurrente recursivo:

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

Hay una serie de razones por las cuales el compilador no puede deducir que este último sea recursivo de cola, pero en el ejemplo que usted ha dado,

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

las mismas reglas se aplican. El compilador no puede determinar que esto es recursivo de cola, pero más aún no puede hacerlo debido a las dos llamadas (ver before y after ).

Lo que parece esperar que el compilador haga con esto es una función que realiza un par de ramas condicionales simples, dos llamadas y algunas cargar / agregar / almacenar.

En cambio, el compilador está desenrollando este ciclo y generando código que tiene muchos puntos de ramificación. Esto se hace en parte porque el compilador cree que será más eficiente de esta manera (involucrando menos ramas) pero en parte porque disminuye la profundidad de recursión del tiempo de ejecución.

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

produce:

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

Para el caso de valores alternativos, asumiendo tamaño == 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]; }

Y imaginemos un caso donde

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

Los dos casos que seleccionó (alternancia y medio patrón) son extremos óptimos y el compilador seleccionó algunos casos intermedios que tratará de manejar mejor.


Eliminando la línea s += A[curIndex+1] + A[size-curIndex-1]; permite la optimización recursiva de la cola . Esta optimización solo puede ocurrir, entonces la llamada recursiva está en la última línea de la función.

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


el problema es este:

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

cada llamada de la función de prueba alterna el resultado de esta comparación, debido a algunas optimizaciones, ya que la matriz es, por ejemplo, 0,0,0,0,0,1,1,1,1

En otras palabras:

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

Pero entonces, la arquitectura del procesador MIGHT (un poder grande, porque depende, para mí esa optimización está desactivada - un i5-6400) tiene una función llamada runahead (realizada a lo largo de la predicción de bifurcación), que ejecuta las instrucciones restantes en la tubería antes de ingresar una rama; por lo que ejecutará test1(size - curIndex -1) antes de la declaración if ofensiva.

Al eliminar la atribución, ingresa a otra optimización, como dijo user1850903.