Intel AVX: version à 256 bits du produit dot pour les variables à virgule flottante de double précision

les Extensions vectorielles avancées Intel (AVX) n'offrent pas de produit dot dans la version de 256 bits (YMM register) pour les variables à virgule flottante de double précision . Le " Pourquoi?"question ont été traités très brièvement dans un autre forum ( ici ) et sur le débordement de la pile ( ici ). Mais la question que je me pose est de savoir comment remplacer efficacement cette instruction manquante par D'autres instructions AVX?

Le produit scalaire en 256-bit version existe pour en virgule flottante simple précision variables ( référence ):

 __m256 _mm256_dp_ps(__m256 m1, __m256 m2, const int mask);

l'idée est de trouver un équivalent efficace pour cette instruction manquante:

 __m256d _mm256_dp_pd(__m256d m1, __m256d m2, const int mask);

pour être plus précis, le code que je voudrais transformer de __m128 (quatre flotteurs) à __m256d (4 doubles) utiliser les instructions suivantes:

   __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) )));

le résultat de ce code est un vecteur _m128 de quatre flotteurs contenant les résultats des produits dot entre val1 et val0 , val2 et val0 , val3 et val0 , val4 et val0 .

peut-être cela peut donner des indices pour les suggestions?

21
demandé sur Peter Mortensen 2012-05-04 22:21:57

3 réponses

j'utiliserais une multiplication double 4*, puis une hadd (qui n'ajoute malheureusement que 2*2 flotteurs dans la moitié supérieure et inférieure), extraire la moitié supérieure (un mélange devrait fonctionner de manière égale, peut-être plus rapide) et l'ajouter à la moitié inférieure.

le résultat est dans le bas 64 bits 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 );

Edit:

Après une idée de Norbert p. j'ai étendu cette version pour faire 4 produits de point à la fois.

__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 );
21
répondu Gunther Piez 2012-05-08 17:50:34

je voudrais étendre drhirsch la réponse de pour effectuer deux points produits dans le même temps, économiser de travail:

__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 );

puis dot(x,y) est dans le double bas et dot(z,w) est dans le double haut de dotproduct .

12
répondu Norbert P. 2017-05-23 12:26:32

pour un produit à point unique, il s'agit simplement d'une somme verticale multipliée et horizontale (voir la façon la plus rapide de faire la somme horizontale du vecteur flottant sur x86 ). hadd coûte 2 shuffles + un add . Il est presque toujours sous-optimal pour le débit lorsqu'il est utilisé avec les deux entrées = le même vecteur.

// 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 vous n'avez besoin que d'un seul produit dot, c'est mieux que la réponse mono-vecteur de @hirschhornsalz par 1 shuffle uop sur Intel, et un plus grande victoire sur AMD Jaguar / Bulldozer-family / Ryzen parce qu'il se réduit à 128b tout de suite au lieu de faire un tas de trucs 256b. L'AMD divise les ops 256b en deux uops 128b.


, Il peut être utile d'utiliser hadd dans des cas comme faisant 2 ou 4 point des produits en parallèle lorsque vous l'utilisez avec 2 différents vecteurs en entrée. dot de Norbert de deux paires de vecteurs semble optimale si vous voulez les résultats emballés. Je ne vois pas de moyen de le faire mieux même avec AVX2 vpermpd comme un passage à niveau.

bien sûr, si vous voulez vraiment un plus grand dot (de 8 ou plus double s), utilisez vertical add (avec accumulateurs multiples pour cacher vaddps latence) et faire l'invocation horizontale à la fin. Vous pouvez également utiliser fma si disponible.


haddpd brassage interne xy et zw ensemble de deux façons différentes et alimente cela à une verticale addpd , et c'est ce que nous ferions à la main de toute façon. Si nous maintenions xy et zw séparés, nous aurions besoin de 2 shuffles + 2 additions pour chacun pour obtenir un produit de point (dans des registres séparés). Donc, en les mélangeant avec hadd comme première étape, nous économisons sur le nombre total de battements, seulement sur les additions et le nombre 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
*/

mais pour AMD, où vextractf128 est très bon marché, et 256b hadd coûte 2x autant que 128b hadd , il pourrait être logique de réduire chaque produit 256b à 128b séparément et puis combiner avec un hadd de 128b.

en fait, selon Agner Fog's tables , haddpd xmm,xmm est 4 uops sur Ryzen. (Et la version 256b ymm est 8 uops). Il est donc préférable d'utiliser 2x vshufpd + vaddpd manuellement sur Ryzen, si ces données sont correctes. Il pourrait ne pas être: ses données pour Piledriver a 3 uop haddpd xmm,xmm , et c'est seulement 4 uops avec un opérande mémoire. Cela n'a pas de sens pour moi qu'ils n'ont pas pu implémenter hadd comme seulement 3 (ou 6 pour ymm) uops.


"1519310920," 4 dot s avec les résultats emballé dans un __m256d , le problème exact demandé, je pense que @hirschhornsalz la réponse est très bon pour les Processeurs Intel. Je ne l'ai pas étudié très attentivement, mais combiner en paires avec hadd est bon. vperm2f128 est efficace sur Intel (mais assez mauvais sur AMD: 8 uops sur Ryzen avec un débit par 3c).

2
répondu Peter Cordes 2017-11-22 23:24:15