tag optimized moz length images for example description alternative optimization assembly vectorization avx2

optimization - moz - seo optimized images



¿En qué situación el AVX2 reuniría instrucciones sería más rápido que cargar los datos individualmente? (1)

Desafortunadamente, las instrucciones de carga recopiladas no son particularmente "inteligentes": parecen generar un ciclo de bus por elemento, independientemente de las direcciones de carga, por lo que incluso si tiene elementos contiguos, aparentemente no existe una lógica interna para unir las cargas. Entonces, en términos de eficiencia, una carga recolectada no es mejor que N cargas escalares, excepto que usa solo una instrucción.

El único beneficio real de las instrucciones de recopilación es cuando implementa código SIMD de todos modos, y necesita cargar datos no contiguos a los que luego va a aplicar otras operaciones SIMD. En ese caso, una instrucción de carga SIMD reunida será mucho más eficiente que un grupo de código escalar que normalmente sería generado, por ejemplo, _mm256_set_xxx() (o un grupo de cargas contiguas y permutas, etc., dependiendo del patrón de acceso real).

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 coma flotante se permuta y se agrega a otra. En c, esto puede implementarse 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 ensamble 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 bucle 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

Analizo estas funciones en una máquina con una CPU Haswell (Xeon E3-1245 v3). Algunos resultados típicos son (tiempos 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-estructurado están muy cerca el uno del otro. (También verifiqué la salida de ensamblaje de gcc, que es bastante similar a mi versión codificada a mano.) La vectorización ofrece algunas ventajas para arreglos pequeños, pero es más lenta para arreglos 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í? ¿Puede alguien proporcionar un ejemplo en el que la instrucción de recopilación de datos realmente proporcione un beneficio de rendimiento en vez de simplemente realizar operaciones de carga múltiple? Si no, ¿cuál es el sentido de tener tal instrucción?

El código de prueba, completo con un makefile para g ++ y nasm, está disponible en https://github.com/vanhala/vectortest.git en caso de que alguien quiera probarlo.