c++ - procesadores - procesador con avx
Intel AVX: versiĆ³n de 256 bits del producto punto para variables de punto flotante de doble precisiĆ³n (3)
Para un solo producto de punto, es simplemente una multiplicación vertical y una suma horizontal (consulte la forma más rápida de hacer la suma del vector de flotación horizontal en x86 ). hadd
cuesta 2 shuffles + un add
. Casi siempre es subóptimo para el rendimiento cuando se usa con ambas entradas = el mismo vector.
// both elements = dot(x,y)
__m128d dot1(__m256d x, __m256d y) {
__m256d xy = _mm256_mul_pd(x, y);
__m128d xylow = _mm256_castps256_pd128(xy); // (__m128d)cast isn''t portable
__m128d xyhigh = _mm256_extractf128_pd(xy, 1);
__m128d sum1 = _mm_add_pd(xylow, xyhigh);
__m128d swapped = _mm_shuffle_pd(sum1, sum1, 0b01); // or unpackhi
__m128d dotproduct = _mm_add_pd(sum1, swapped);
return dotproduct;
}
Si solo necesita un producto de puntos, esto es mejor que la respuesta de un solo vector de @ hirschhornsalz por 1 shuffle uop en Intel, y una mayor victoria en AMD Jaguar / Bulldozer-family / Ryzen porque se reduce a 128b de inmediato en lugar de hacer un montón de cosas 256b. AMD divide 256b ops en dos 128b uops.
Puede valer la pena usar hadd
en casos como hacer productos de 2 o 4 puntos en paralelo, donde lo estás usando con 2 vectores de entrada diferentes. El dot
de Norbert de dos pares de vectores se ve óptimo si desea que los resultados se empaqueten. No veo ninguna manera de hacerlo mejor incluso con AVX2 vpermpd
como una vpermpd
aleatoria de vías.
Por supuesto, si realmente desea un dot
más grande (de 8 o más double
s), use la add
vertical (con múltiples acumuladores para ocultar la latencia de vaddps
) y haga la suma horizontal al final. También puede utilizar fma
si está disponible.
haddpd
xy
y zw
dos maneras diferentes y las addpd
a un addpd
vertical, y eso es lo que haríamos a mano de todos modos. Si mantuviéramos xy
y zw
separados, necesitaríamos 2 shuffles + 2 adiciones para que cada uno obtenga un producto de puntos (en registros separados). Así que al mezclarlos junto con hadd
como primer paso, ahorramos en el número total de cambios aleatorios, solo en las sumas y el recuento total de uop.
/* Norbert''s version, for an Intel CPU:
__m256d temp = _mm256_hadd_pd( xy, zw ); // 2 shuffle + 1 add
__m128d hi128 = _mm256_extractf128_pd( temp, 1 ); // 1 shuffle (lane crossing, higher latency)
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 ); // 1 add
// 3 shuffle + 2 add
*/
Pero para AMD, donde vextractf128
es muy barato y 256b hadd
cuesta 2 veces más que 128b hadd
, podría tener sentido reducir cada producto 256b a 128b por separado y luego combinarlo con un 128b hadd.
En realidad, según las tablas de Agner Fog , haddpd xmm,xmm
son 4 puntos en Ryzen. (Y la versión de 256b ymm es 8 uops). Entonces, en realidad es mejor usar 2x vshufpd
+ vaddpd
manualmente en Ryzen, si esos datos son correctos. Puede que no lo sea: sus datos para Piledriver tienen 3 uop haddpd xmm,xmm
, y solo son 4 uops con un operando de memoria. No tiene sentido para mí que no pudieran implementar hadd
como solo 3 (o 6 para ymm) uops.
Para hacer 4 dot
con los resultados agrupados en un __m256d
, el problema exacto preguntado, creo que la respuesta de @ hirschhornsalz parece muy buena para las CPU de Intel. No lo he estudiado con mucho cuidado, pero combinar en pares con hadd
es bueno. vperm2f128
es eficiente en Intel (pero bastante malo en AMD: 8 Ups en Ryzen con uno por rendimiento de 3c).
Intel Advanced Vector Extensions (AVX) no ofrece productos de puntos en la versión de 256 bits (registro YMM) para variables de punto flotante de doble precisión . ¿El porque?" La pregunta se ha tratado brevemente en otro foro ( here ) y en Desbordamiento de pila ( here ). ¿Pero la pregunta que me enfrento es cómo reemplazar esta instrucción faltante con otras instrucciones AVX de manera eficiente?
El producto de puntos en la versión de 256 bits existe para las variables de punto flotante de precisión simple ( consulte aquí ):
__m256 _mm256_dp_ps(__m256 m1, __m256 m2, const int mask);
La idea es encontrar un equivalente eficiente para esta instrucción faltante:
__m256d _mm256_dp_pd(__m256d m1, __m256d m2, const int mask);
Para ser más específico, el código que me gustaría transformar de __m128
(cuatro flotantes) a __m256d
(4 dobles) usa las siguientes instrucciones:
__m128 val0 = ...; // Four float values
__m128 val1 = ...; //
__m128 val2 = ...; //
__m128 val3 = ...; //
__m128 val4 = ...; //
__m128 res = _mm_or_ps( _mm_dp_ps(val1, val0, 0xF1),
_mm_or_ps( _mm_dp_ps(val2, val0, 0xF2),
_mm_or_ps( _mm_dp_ps(val3, val0, 0xF4),
_mm_dp_ps(val4, val0, 0xF8) )));
El resultado de este código es un vector _m128
de cuatro flotadores que contienen los resultados de los productos de puntos entre val1
y val0
, val2
y val0
, val3
y val0
, val4
y val0
.
Tal vez esto puede dar pistas para las sugerencias?
Yo usaría una multiplicación doble de 4 *, luego un hadd
(que desafortunadamente agrega solo 2 * 2 flotadores en la mitad superior e inferior), extrae la mitad superior (un shuffle debería funcionar igual, quizás más rápido) y lo agrega a la mitad inferior .
El resultado está en el bajo 64 bit de dotproduct
.
__m256d xy = _mm256_mul_pd( x, y );
__m256d temp = _mm256_hadd_pd( xy, xy );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );
Editar:
Después de una idea de Norbert P. extendí esta versión para hacer productos de 4 puntos a la vez.
__m256d xy0 = _mm256_mul_pd( x[0], y[0] );
__m256d xy1 = _mm256_mul_pd( x[1], y[1] );
__m256d xy2 = _mm256_mul_pd( x[2], y[2] );
__m256d xy3 = _mm256_mul_pd( x[3], y[3] );
// low to high: xy00+xy01 xy10+xy11 xy02+xy03 xy12+xy13
__m256d temp01 = _mm256_hadd_pd( xy0, xy1 );
// low to high: xy20+xy21 xy30+xy31 xy22+xy23 xy32+xy33
__m256d temp23 = _mm256_hadd_pd( xy2, xy3 );
// low to high: xy02+xy03 xy12+xy13 xy20+xy21 xy30+xy31
__m256d swapped = _mm256_permute2f128_pd( temp01, temp23, 0x21 );
// low to high: xy00+xy01 xy10+xy11 xy22+xy23 xy32+xy33
__m256d blended = _mm256_blend_pd(temp01, temp23, 0b1100);
__m256d dotproduct = _mm256_add_pd( swapped, blended );
Extendería la respuesta de drhirsch para realizar dos productos de puntos al mismo tiempo, ahorrando algo de trabajo:
__m256d xy = _mm256_mul_pd( x, y );
__m256d zw = _mm256_mul_pd( z, w );
__m256d temp = _mm256_hadd_pd( xy, zw );
__m128d hi128 = _mm256_extractf128_pd( temp, 1 );
__m128d dotproduct = _mm_add_pd( (__m128d)temp, hi128 );
Luego el dot(x,y)
está en el doble bajo y el dot(z,w)
está en el doble alto del producto dotproduct
.