Ho studiato l'uso delle nuove istruzioni di raccolta del set di istruzioni AVX2. Nello specifico, ho deciso di confrontare un semplice problema, in cui una matrice in virgola mobile è permutata e aggiunta a un'altra. In c, questo può essere implementato comeIn quale situazione le istruzioni AVX2 potrebbero essere più veloci del caricamento individuale dei dati?
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 questa funzione con g ++ -O3 -march = native. Ora, lo implemento in assemblaggio in tre modi. Per semplicità suppongo che la lunghezza degli array N sia divisibile per quattro. La semplice, non vettorizzati implementazione:
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
Il ciclo Vectorised senza l'istruzione di raccogliere:
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, infine, un'implementazione con 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
I benchmark queste funzioni su una macchina con una CPU Haswell (Xeon E3-1245 v3). Alcuni risultati tipici sono (volte in secondi):
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
Il gcc e la versione assemblaggio nonvectorized sono molto vicini l'uno all'altro. (Ho anche controllato l'output di assembly di gcc, che è abbastanza simile alla mia versione codificata a mano.) La vettorizzazione offre alcuni vantaggi per i piccoli array, ma è più lenta per i grandi array. La grande sorpresa (almeno per me) è che la versione che utilizza vgatherpdp sia così lenta. Quindi, la mia domanda è, perché? Sto facendo qualcosa di stupido qui? Qualcuno può fornire un esempio in cui l'istruzione di raccolta potrebbe effettivamente fornire un vantaggio sulle prestazioni semplicemente eseguendo più operazioni di caricamento? In caso contrario, qual è il punto di avere effettivamente una tale istruzione?
Il codice di prova, completo di un makefile per g ++ e nasm, è disponibile al https://github.com/vanhala/vectortest.git nel caso qualcuno voglia provare questo.
Bene, non sorprende che le funzioni codificate a mano siano più veloci, il compilatore C deve produrre * codice corretto *, dopotutto. I vostri loop non prevedono la lunghezza di array che non è un multiplo della dimensione di vettorizzazione e non controllano nemmeno se il conteggio fosse zero ... – EOF
@EOF Sì, ma questo è accanto al punto. Il punto principale di questo benchmark era confrontare l'efficienza delle istruzioni di caricamento raccolte rispetto all'implementazione della stessa cosa usando i carichi scalari. La versione generata dal compilatore era lì solo per essere sicuro che i tempi fossero nel giusto ambito, cioè per controllare che non stia facendo nulla di completamente stupido nelle versioni codificate a mano. – infinitesimal