Em que situação o AVX2 coletaria instruções seria mais rápido do que carregar os dados individualmente?

Eu tenho investigado o uso das novas instruções de coleta do conjunto de instruções AVX2. Especificamente, decidi comparar um problema simples, em que uma matriz de ponto flutuante é permutada e adicionada a outra. Em c, isso pode ser implementado 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]];
    }
}

Eu compilo essa função com g ++ -O3 -march = native. Agora, eu implemento isso em montagem de três maneiras. Por simplicidade, assumo que o comprimento das matrizes N é divisível por quatro. A implementação simples e não vetorizada:

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

O loop vetorizou sem a instrução de coleta:

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

E, finalmente, uma implementação 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

Eu comparo essas funções em uma máquina com um processador Haswell (Xeon E3-1245 v3). Alguns resultados típicos são (vezes em 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

O gcc e a versão de montagem não-vetorizada estão muito próximas uma da outra. (Também verifiquei a saída do assembly do gcc, que é bastante semelhante à minha versão codificada manualmente.) A vetorização oferece alguns benefícios para matrizes pequenas, mas é mais lenta para matrizes grandes. A grande surpresa (pelo menos para mim) é que a versão usando vgatherpdp é muito lenta. Então, minha pergunta é: por quê? Estou fazendo algo estúpido aqui?Alguém pode fornecer um exemplo em que a instrução de coleta realmente traria um benefício de desempenho ao executar várias operações de carregamento? Caso contrário, qual é o sentido de realmente ter essa instrução?

O código de teste, completo com um makefile para g ++ e nasm, está disponível emhttps://github.com/vanhala/vectortest.git caso alguém queira tentar isso.

questionAnswers(2)

yourAnswerToTheQuestion