¿En qué situación el AVX2 recopilará instrucciones más rápido que cargar los datos individualmente?

He estado investigando el uso de las nuevas instrucciones de recopilación del conjunto de instrucciones AVX2. Específicamente, decidí comparar un problema simple, donde una matriz de punto flotante se permuta y se agrega a otra. En c, esto se puede implementar como

void vectortest(double * a,double * b,unsigned int * ind,unsigned int N)
{
    int i;
    for(i=0;i<N;++i)
    {
        a[i]+=b[ind[i]];
    }
}

Compilo esta función con g ++ -O3 -march = native. Ahora, implemento esto en ensamblaje de tres maneras. Por simplicidad, supongo que la longitud de las matrices N es divisible por cuatro. La implementación simple, no vectorizada:

align 4
global vectortest_asm
vectortest_asm:
        ;;  double * a = rdi                                                                                                                                                                                                                                   
        ;;  double * b = rsi                                                                                                                                                                                                                                   
        ;;  unsigned int * ind = rdx                                                                                                                                                                                                                           
        ;;  unsigned int N = rcx                                                                                                                                                                                                                               

        push rax
        xor rax,rax

loop:   sub rcx, 1
        mov eax, [rdx+rcx*4]    ;eax = ind[rcx]                                                                                                                                                                                                                
        vmovq xmm0, [rdi+rcx*8]         ;xmm0 = a[rcx]                                                                                                                                                                                                         
        vaddsd xmm0, [rsi+rax*8]        ;xmm1 += b[rax] ( and b[rax] = b[eax] = b[ind[rcx]])                                                                                                                                                                   
        vmovq [rdi+rcx*8], xmm0
        cmp rcx, 0
        jne loop

        pop rax

        ret

El ciclo vectorizado sin la instrucción de recopilación:

loop:   sub rcx, 4

        mov eax,[rdx+rcx*4]    ;first load the values from array b to xmm1-xmm4
        vmovq xmm1,[rsi+rax*8]
        mov eax,[rdx+rcx*4+4]
        vmovq xmm2,[rsi+rax*8]

        mov eax,[rdx+rcx*4+8]
        vmovq xmm3,[rsi+rax*8]
        mov eax,[rdx+rcx*4+12]
        vmovq xmm4,[rsi+rax*8]

        vmovlhps xmm1,xmm2     ;now collect them all to ymm1
        vmovlhps xmm3,xmm4
        vinsertf128 ymm1,ymm1,xmm3,1

        vaddpd ymm1, ymm1, [rdi+rcx*8]
        vmovupd [rdi+rcx*8], ymm1

        cmp rcx, 0
        jne loop

Y finalmente, una implementación usando vgatherdpd:

loop:   sub rcx, 4               
        vmovdqu xmm2,[rdx+4*rcx]           ;load the offsets from array ind to xmm2
        vpcmpeqw ymm3,ymm3                 ;set ymm3 to all ones, since it acts as the mask in vgatherdpd                                                                                                                                                                 
        vgatherdpd ymm1,[rsi+8*xmm2],ymm3  ;now gather the elements from array b to ymm1

        vaddpd ymm1, ymm1, [rdi+rcx*8]
        vmovupd [rdi+rcx*8], ymm1

        cmp rcx, 0
        jne loop

Comparo estas funciones en una máquina con una CPU Haswell (Xeon E3-1245 v3). Algunos resultados típicos son (veces en segundos):

Array length 100, function called 100000000 times.
Gcc version: 6.67439
Nonvectorized assembly implementation: 6.64713
Vectorized without gather: 4.88616
Vectorized with gather: 9.32949

Array length 1000, function called 10000000 times.
Gcc version: 5.48479
Nonvectorized assembly implementation: 5.56681
Vectorized without gather: 4.70103
Vectorized with gather: 8.94149

Array length 10000, function called 1000000 times.
Gcc version: 7.35433
Nonvectorized assembly implementation: 7.66528
Vectorized without gather: 7.92428
Vectorized with gather: 8.873

El gcc y la versión de ensamblaje no vectorizado están muy cerca el uno del otro. (También verifiqué la salida del ensamblaje de gcc, que es bastante similar a mi versión codificada a mano). La vectorización ofrece algunos beneficios para las matrices pequeñas, pero es más lenta para las matrices grandes. La gran sorpresa (al menos para mí) es que la versión que usa vgatherpdp es muy lenta. ¿Mi pregunta es, porque? ¿Estoy haciendo algo estúpido aquí?¿Alguien puede proporcionar un ejemplo en el que la instrucción de recopilación realmente otorgue un beneficio de rendimiento en lugar de simplemente realizar múltiples operaciones de carga? Si no, ¿cuál es el punto de tener tal instrucción?

El código de prueba, completo con un archivo MAKE para g ++ y nasm, está disponible enhttps://github.com/vanhala/vectortest.git en caso de que alguien quiera probar esto.

Respuestas a la pregunta(2)

Su respuesta a la pregunta