web-dev-qa-db-fra.com

AVX2 quel est le moyen le plus efficace d'emballer les restes en fonction d'un masque?

Si vous avez un tableau d'entrée et un tableau de sortie, mais que vous souhaitez uniquement écrire les éléments qui remplissent une certaine condition, quelle serait la façon la plus efficace de le faire dans AVX2?

J'ai vu dans SSE où cela a été fait comme ceci: (De: https://deplinenoise.files.wordpress.com/2015/03/gdc2015_afredriksson_simd.pdf =)

__m128i LeftPack_SSSE3(__m128 mask, __m128 val)
{
 // Move 4 sign bits of mask to 4-bit integer value.
 int mask = _mm_movemask_ps(mask);
 // Select shuffle control data
 __m128i shuf_ctrl = _mm_load_si128(&shufmasks[mask]);
 // Permute to move valid values to front of SIMD register
 __m128i packed = _mm_shuffle_epi8(_mm_castps_si128(val), shuf_ctrl);
 return packed;
}

Cela semble correct pour SSE qui est de 4 larges, et n'a donc besoin que d'une LUT de 16 entrées, mais pour AVX qui est de 8 de large, la LUT devient assez grande (256 entrées, chacune de 32 octets, ou 8k).

Je suis surpris qu'AVX ne semble pas avoir d'instructions pour simplifier ce processus, comme un magasin masqué avec emballage.

Je pense qu'avec un peu de mélange pour compter le nombre de bits de signe définis à gauche, vous pouvez générer la table de permutation nécessaire, puis appeler _mm256_permutevar8x32_ps. Mais c'est aussi pas mal d'instructions je pense ..

Quelqu'un connaît-il des astuces pour le faire avec AVX2? Ou quelle est la méthode la plus efficace?

Voici une illustration du problème d'emballage gauche du document ci-dessus:

Left.Packing.Problem

Merci

31
Froglegs

AVX2 + BMI2. Voir mon autre réponse pour AVX512. (Mise à jour: enregistré un pdep dans les versions 64 bits.)

Nous pouvons utiliser AVX2 vpermps (_mm256_permutevar8x32_ps) (ou l'équivalent entier, vpermd) pour effectuer un shuffle variable de franchissement de voie.

Nous pouvons générer des masques à la volée , car BMI2 pext (Extraction de bits parallèles) nous fournit un version bit à bit de l'opération dont nous avons besoin.

Attention, pdep/pext sont très lent sur les processeurs AMD, comme 6 uops/18 cycles de latence et de débit sur Ryzen . Cette implémentation fonctionnera horriblement sur AMD. Pour AMD, vous pouvez être mieux avec des vecteurs 128 bits en utilisant une pshufb ou vpermilps LUT, ou certaines des suggestions de décalage variable AVX2 discutées dans les commentaires si votre entrée de masque est un masque vectoriel ( pas un masque de bits déjà calculé à partir de la mémoire). AMD avant Zen2 n'a de toute façon que des unités d'exécution vectorielles 128 bits, et les shuffles de franchissement de voie 256 bits sont lents. Les vecteurs 128 bits sont donc très attractifs pour cela sur AMD actuel.


Pour les vecteurs entiers avec des éléments 32 bits ou plus : Soit 1) _mm256_movemask_ps(_mm256_castsi256_ps(compare_mask)).
Ou 2) utilisez _mm256_movemask_epi8, Puis modifiez la première constante PDEP de 0x010101010101010101 à 0x0F0F0F0F0F0F0F0F pour diffuser des blocs de 4 bits contigus. Modifiez la multiplication par 0xFFU en expanded_mask |= expanded_mask<<4; Ou expanded_mask *= 0x11; (Non testé). Dans les deux cas, utilisez le masque aléatoire avec VPERMD au lieu de VPERMPS.

Pour les éléments 64 bits ou double, tout fonctionne toujours correctement ; Il se trouve que le masque de comparaison a toujours des paires d'éléments 32 bits identiques, de sorte que le shuffle résultant place les deux moitiés de chaque élément 64 bits au bon endroit. (Vous utilisez donc toujours VPERMPS ou VPERMD, car VPERMPD et VPERMQ ne sont disponibles qu'avec des opérandes de contrôle immédiat.)

Pour les éléments 16 bits, vous pourrez peut-être l'adapter avec des vecteurs 128 bits.


L'algorithme:

Commencez avec une constante d'indices 3 bits compressés, chaque position contenant son propre index. c'est-à-dire [ 7 6 5 4 3 2 1 0 ] où chaque élément a une largeur de 3 bits. 0b111'110'101'...'010'001'000.

Utilisez pext pour extraire les indices que nous voulons dans une séquence contiguë au bas d'un registre entier. par exemple. si nous voulons les indices 0 et 2, notre masque de contrôle pour pext devrait être 0b000'...'111'000'111. pext récupérera les groupes d'index 010 et 000 qui s'alignent avec les 1 bits du sélecteur. Les groupes sélectionnés sont regroupés dans les bits bas de la sortie, la sortie sera donc 0b000'...'010'000. (c'est-à-dire [ ... 2 0 ])

Voir le code commenté pour savoir comment générer l'entrée 0b111000111 Pour pext à partir du masque vectoriel d'entrée.

Maintenant, nous sommes dans le même bateau que la LUT compressée: décompressez jusqu'à 8 indices emballés.

Au moment où vous assemblez toutes les pièces, il y a trois pext/pdeps au total. J'ai travaillé à l'envers de ce que je voulais, donc c'est probablement plus facile de le comprendre dans cette direction aussi. (c'est-à-dire commencer par la ligne de lecture aléatoire et continuer à partir de là.)

Nous pouvons simplifier le décompactage si nous travaillons avec des indices un par octet au lieu de groupes compressés de 3 bits . Puisque nous avons 8 indices, cela n'est possible qu'avec du code 64 bits.

Voir ceci et une version 32 bits uniquement sur Godbolt Compiler Explorer . J'ai utilisé #ifdef Pour qu'il se compile de manière optimale avec -m64 Ou -m32. gcc gaspille quelques instructions, mais clang fait vraiment du bon code.

#include <stdint.h>
#include <immintrin.h>

// Uses 64bit pdep / pext to save a step in unpacking.
__m256 compress256(__m256 src, unsigned int mask /* from movmskps */)
{
  uint64_t expanded_mask = _pdep_u64(mask, 0x0101010101010101);  // unpack each bit to a byte
  expanded_mask *= 0xFF;    // mask |= mask<<1 | mask<<2 | ... | mask<<7;
  // ABC... -> AAAAAAAABBBBBBBBCCCCCCCC...: replicate each bit to fill its byte

  const uint64_t identity_indices = 0x0706050403020100;    // the identity shuffle for vpermps, packed to one index per byte
  uint64_t wanted_indices = _pext_u64(identity_indices, expanded_mask);

  __m128i bytevec = _mm_cvtsi64_si128(wanted_indices);
  __m256i shufmask = _mm256_cvtepu8_epi32(bytevec);

  return _mm256_permutevar8x32_ps(src, shufmask);
}

Cela se compile pour coder sans charge de mémoire, uniquement des constantes immédiates. (Voir le lien Godbolt pour cela et la version 32 bits).

    # clang 3.7.1 -std=gnu++14 -O3 -march=haswell
    mov     eax, edi                   # just to zero extend: goes away when inlining
    movabs  rcx, 72340172838076673     # The constants are hoisted after inlining into a loop
    pdep    rax, rax, rcx              # ABC       -> 0000000A0000000B....
    imul    rax, rax, 255              # 0000000A0000000B.. -> AAAAAAAABBBBBBBB..
    movabs  rcx, 506097522914230528
    pext    rax, rcx, rax
    vmovq   xmm1, rax
    vpmovzxbd       ymm1, xmm1         # 3c latency since this is lane-crossing
    vpermps ymm0, ymm1, ymm0
    ret

Donc, selon les nombres d'Agner Fog , c'est 6 uops (sans compter les constantes, ou le mov à extension zéro qui disparaît lorsqu'il est en ligne). Sur Intel Haswell, c'est une latence 16c (1 pour vmovq, 3 pour chaque pdep/imul/pext/vpmovzx/vpermps). Il n'y a pas de parallélisme au niveau de l'instruction. Dans une boucle où cela ne fait pas partie d'une dépendance portée par une boucle, cependant (comme celle que j'ai incluse dans le lien Godbolt), le goulot d'étranglement est, espérons-le, juste le débit, gardant plusieurs itérations de cela en vol à la fois.

Cela peut peut-être gérer un débit d'un par 3 cycles, goulot d'étranglement sur le port1 pour pdep/pext/imul. Bien sûr, avec les charges/magasins et la surcharge de boucle (y compris la comparaison, movmsk et popcnt), le débit uop total peut facilement être un problème. (par exemple, la boucle de filtrage dans mon lien godbolt est de 14 uops avec clang, avec -fno-unroll-loops pour le rendre plus facile à lire. Il pourrait soutenir une itération par 4c, en suivant le front-end, si nous avons de la chance , mais je pense que clang n'a pas réussi à expliquer la fausse dépendance de popcnt à sa sortie, donc il goulot d'étranglement aux 3/5ème de la latence de la fonction compress256.)

gcc effectue la multiplication par 0xFF avec plusieurs instructions, en utilisant un décalage gauche de 8 et un sub. Cela nécessite des instructions supplémentaires de mov, mais le résultat final est une multiplication avec une latence de 2. (Haswell gère mov au stade du changement de nom de registre avec une latence nulle.)


Étant donné que tout le matériel prenant en charge AVX2 prend également en charge BMI2, il est probablement inutile de fournir une version pour AVX2 sans BMI2.

Si vous avez besoin de le faire dans une boucle très longue, la LUT en vaut probablement la peine si les ratés de cache initiaux sont amortis sur suffisamment d'itérations avec la surcharge inférieure de simplement déballer l'entrée LUT. Vous devez toujours movmskps, vous pouvez donc popcnt le masque et l'utiliser comme un index LUT, mais vous enregistrez un pdep/imul/pexp.

Vous pouvez décompresser les entrées LUT avec la même séquence d'entier que j'ai utilisée, mais set1()/vpsrlvd/vpand de @ Froglegs est probablement meilleur lorsque l'entrée LUT démarre en mémoire et ne le fait pas besoin d'entrer dans des registres entiers en premier lieu. (Une charge de diffusion 32 bits n'a pas besoin d'un uop ALU sur les processeurs Intel). Cependant, un décalage variable est de 3 uops sur Haswell (mais seulement 1 sur Skylake).

25
Peter Cordes

Si vous ciblez AMD Zen, cette méthode peut être préférée, en raison du très lent pdepand pext sur ryzen (18 cycles chacun).

J'ai trouvé cette méthode, qui utilise une LUT compressée, qui est de 768 (+1 padding) octets, au lieu de 8k. Il nécessite une diffusion d'une seule valeur scalaire, qui est ensuite décalée d'une quantité différente dans chaque voie, puis masquée sur les 3 bits inférieurs, ce qui fournit une LUT de 0 à 7.

Voici la version intrinsèque, ainsi que le code pour construire LUT.

//Generate Move mask via: _mm256_movemask_ps(_mm256_castsi256_ps(mask)); etc
__m256i MoveMaskToIndices(u32 moveMask) {
    u8 *adr = g_pack_left_table_u8x3 + moveMask * 3;
    __m256i indices = _mm256_set1_epi32(*reinterpret_cast<u32*>(adr));//lower 24 bits has our LUT

   // __m256i m = _mm256_sllv_epi32(indices, _mm256_setr_epi32(29, 26, 23, 20, 17, 14, 11, 8));

    //now shift it right to get 3 bits at bottom
    //__m256i shufmask = _mm256_srli_epi32(m, 29);

    //Simplified version suggested by wim
    //shift each lane so desired 3 bits are a bottom
    //There is leftover data in the lane, but _mm256_permutevar8x32_ps  only examines the first 3 bits so this is ok
    __m256i shufmask = _mm256_srlv_epi32 (indices, _mm256_setr_epi32(0, 3, 6, 9, 12, 15, 18, 21));
    return shufmask;
}

u32 get_nth_bits(int a) {
    u32 out = 0;
    int c = 0;
    for (int i = 0; i < 8; ++i) {
        auto set = (a >> i) & 1;
        if (set) {
            out |= (i << (c * 3));
            c++;
        }
    }
    return out;
}
u8 g_pack_left_table_u8x3[256 * 3 + 1];

void BuildPackMask() {
    for (int i = 0; i < 256; ++i) {
        *reinterpret_cast<u32*>(&g_pack_left_table_u8x3[i * 3]) = get_nth_bits(i);
    }
}

Voici l'assembly généré par MSVC:

  lea ecx, DWORD PTR [rcx+rcx*2]
  lea rax, OFFSET FLAT:unsigned char * g_pack_left_table_u8x3 ; g_pack_left_table_u8x3
  vpbroadcastd ymm0, DWORD PTR [rcx+rax]
  vpsrlvd ymm0, ymm0, YMMWORD PTR __ymm@00000015000000120000000f0000000c00000009000000060000000300000000
7
Froglegs

Voir mon autre réponse pour AVX2 + BMI2 sans LUT.

Puisque vous mentionnez un problème d'évolutivité vers AVX512: ne vous inquiétez pas, il y a une instruction AVX512F pour exactement cela :

VCOMPRESSPS - Stocker des valeurs à virgule flottante simple précision éparses dans une mémoire dense . (Il existe également des versions pour les éléments entiers doubles et 32 ​​ou 64 bits (vpcompressq), mais pas pour les octets ou Word (16 bits)). C'est comme BMI2 pdep/pext, mais pour les éléments vectoriels au lieu des bits dans un entier reg.

La destination peut être un registre vectoriel ou un opérande mémoire, tandis que la source est un registre vectoriel et un masque. Avec un registre dest, il peut fusionner ou remettre à zéro les bits supérieurs. Avec une mémoire dest, "Seul le vecteur contigu est écrit à l'emplacement de mémoire de destination".

Pour déterminer jusqu'où faire avancer votre pointeur pour le vecteur suivant, ouvrez le masque.

Disons que vous voulez filtrer tout sauf les valeurs> = 0 d'un tableau:

#include <stdint.h>
#include <immintrin.h>
size_t filter_non_negative(float *__restrict__ dst, const float *__restrict__ src, size_t len) {
    const float *endp = src+len;
    float *dst_start = dst;
    do {
        __m512      sv  = _mm512_loadu_ps(src);
        __mmask16 keep = _mm512_cmp_ps_mask(sv, _mm512_setzero_ps(), _CMP_GE_OQ);  // true for src >= 0.0, false for unordered and src < 0.0
        _mm512_mask_compressstoreu_ps(dst, keep, sv);   // clang is missing this intrinsic, which can't be emulated with a separate store

        src += 16;
        dst += _mm_popcnt_u64(keep);   // popcnt_u64 instead of u32 helps gcc avoid a wasted movsx, but is potentially slower on some CPUs
    } while (src < endp);
    return dst - dst_start;
}

Cela compile (avec gcc4.9 ou version ultérieure) en ( Godbolt Compiler Explorer ):

 # Output from gcc6.1, with -O3 -march=haswell -mavx512f.  Same with other gcc versions
    lea     rcx, [rsi+rdx*4]             # endp
    mov     rax, rdi
    vpxord  zmm1, zmm1, zmm1             # vpxor  xmm1, xmm1,xmm1 would save a byte, using VEX instead of EVEX
.L2:
    vmovups zmm0, ZMMWORD PTR [rsi]
    add     rsi, 64
    vcmpps  k1, zmm0, zmm1, 29           # AVX512 compares have mask regs as a destination
    kmovw   edx, k1                      # There are some insns to add/or/and mask regs, but not popcnt
    movzx   edx, dx                      # gcc is dumb and doesn't know that kmovw already zero-extends to fill the destination.
    vcompressps     ZMMWORD PTR [rax]{k1}, zmm0
    popcnt  rdx, rdx
    ## movsx   rdx, edx         # with _popcnt_u32, gcc is dumb.  No casting can get gcc to do anything but sign-extend.  You'd expect (unsigned) would mov to zero-extend, but no.
    lea     rax, [rax+rdx*4]             # dst += ...
    cmp     rcx, rsi
    ja      .L2

    sub     rax, rdi
    sar     rax, 2                       # address math -> element count
    ret

Performances: les vecteurs 256 bits peuvent être plus rapides sur Skylake-X/Cascade Lake

En théorie, une boucle qui charge une image bitmap et filtre un tableau dans un autre doit s'exécuter à 1 vecteur pour 3 horloges sur SKX/CSLX, quelle que soit la largeur du vecteur, goulot d'étranglement sur le port 5. (kmovb/w/d/q k1, eax s'exécute sur p5, et vcompressps en mémoire est 2p5 + un magasin, selon IACA et selon les tests de http://uops.info/ ).

@ZachB rapporte dans les commentaires qu'en pratique, une boucle utilisant ZMM _mm512_mask_compressstoreu_ps est légèrement plus lent que _mm256_mask_compressstoreu_ps sur du vrai matériel CSLX. (Je ne suis pas sûr que ce soit une microbenchmark qui permettrait à la version 256 bits de sortir du "mode vectoriel 512 bits" et de cadencer plus haut, ou s'il y avait du code 512 bits.)

Je soupçonne que les magasins mal alignés nuisent à la version 512 bits. vcompressps fait probablement efficacement une mémoire vectorielle masquée de 256 ou 512 bits, et si cela traverse une limite de ligne de cache, alors il doit faire un travail supplémentaire . Étant donné que le pointeur de sortie n'est généralement pas un multiple de 16 éléments, une mémoire de 512 bits pleine ligne sera presque toujours mal alignée.

Les magasins 512 bits désalignés peuvent être pires que les magasins 256 bits divisés en lignes de cache pour une raison quelconque, et se produire plus souvent; nous savons déjà que la vectorisation 512 bits d'autres choses semble être plus sensible à l'alignement. Cela peut simplement être dû au manque de tampons de charge fractionnée lorsqu'ils se produisent à chaque fois, ou peut-être que le mécanisme de secours pour gérer les séparations de ligne de cache est moins efficace pour les vecteurs 512 bits.

Il serait intéressant de comparer vcompressps dans un registre, avec des magasins de chevauchement de vecteur complet séparés . C'est probablement la même chose, mais le magasin peut micro-fusionner lorsqu'il s'agit d'une instruction distincte. Et s'il y a une différence entre les magasins masqués et les magasins qui se chevauchent, cela le révélerait.


Une autre idée discutée dans les commentaires ci-dessous était d'utiliser vpermt2ps pour créer des vecteurs complets pour les magasins alignés. Cela ce serait difficile à faire sans branchement , et le branchement lorsque nous remplissons un vecteur sera probablement mal interprété à moins que le masque de bits ait un modèle assez régulier, ou de grandes séries de all-0 et all-1.

Une implémentation sans branche avec une chaîne de dépendances en boucle de 4 ou 6 cycles à travers le vecteur en cours de construction pourrait être possible, avec un vpermt2ps et un mélange ou quelque chose pour le remplacer quand il est "plein". Avec un vecteur aligné, stockez chaque itération, mais déplacez le pointeur de sortie uniquement lorsque le vecteur est plein.

Ceci est probablement plus lent que vcompressps avec des magasins non alignés sur les processeurs Intel actuels.

7
Peter Cordes

Dans le cas où quelqu'un serait intéressé, voici une solution pour SSE2 qui utilise une instruction LUT au lieu d'une LUT de données alias une table de saut. Avec AVX, cela nécessiterait cependant 256 cas.

Chaque fois que vous appelez LeftPack_SSE2 ci-dessous, il utilise essentiellement trois instructions: jmp, shufps, jmp. Cinq des seize cas n'ont pas besoin de modifier le vecteur.

static inline __m128 LeftPack_SSE2(__m128 val, int mask)  {
  switch(mask) {
  case  0:
  case  1: return val;
  case  2: return _mm_shuffle_ps(val,val,0x01);
  case  3: return val;
  case  4: return _mm_shuffle_ps(val,val,0x02);
  case  5: return _mm_shuffle_ps(val,val,0x08);
  case  6: return _mm_shuffle_ps(val,val,0x09);
  case  7: return val;
  case  8: return _mm_shuffle_ps(val,val,0x03);
  case  9: return _mm_shuffle_ps(val,val,0x0c);
  case 10: return _mm_shuffle_ps(val,val,0x0d);
  case 11: return _mm_shuffle_ps(val,val,0x34);
  case 12: return _mm_shuffle_ps(val,val,0x0e);
  case 13: return _mm_shuffle_ps(val,val,0x38);
  case 14: return _mm_shuffle_ps(val,val,0x39);
  case 15: return val;
  }
}

__m128 foo(__m128 val, __m128 maskv) {
  int mask = _mm_movemask_ps(maskv);
  return LeftPack_SSE2(val, mask);
}
6
Z boson