Comment fusionner un scalaire dans un vecteur sans que le compilateur gaspille une instruction mettant à zéro les éléments supérieurs? Limite de conception dans les composants insortingnsèques d’Intel?

Je n’ai pas un cas d’utilisation particulier à l’esprit; Je demande s’il s’agit vraiment d’un défaut de conception / d’une limitation des parameters insortingnsèques d’Intel ou s’il me manque quelque chose.

Si vous souhaitez combiner un flottant scalaire avec un vecteur existant, il ne semble pas y avoir de moyen de le faire sans la réduction à zéro des éléments ou la diffusion du scalaire en vecteur, à l’aide des composants insortingnsèques d’Intel. Je n’ai pas étudié les extensions de vecteur natif GNU C et les fonctions intégrées associées.

Cela ne serait pas trop mal si l’optique insortingnsèque était optimisée, mais pas avec gcc (5.4 ou 6.2). Il n’existe pas non plus de moyen pratique d’utiliser pmovzx ou insertps tant que charges, pour la raison connexe, à savoir que leurs éléments insortingnsèques ne prennent que des arguments de vecteur. (Et gcc ne plie pas une charge scalaire-> vectorielle dans l’instruction asm.)

 __m128 replace_lower_two_elements(__m128 v, float x) { __m128 xv = _mm_set_ss(x); // WANTED: something else for this step, some comstackrs actually comstack this to a separate insn return _mm_shuffle_ps(v, xv, 0); // lower 2 elements are both x, and the garbage is gone } 

gcc 5.3 -march = nehalem -O3 output, pour activer SSE4.1 et régler ce processeur Intel: C’est encore pire sans SSE4.1; plusieurs instructions vous permettent de mettre à zéro les éléments supérieurs.

  insertps xmm1, xmm1, 0xe # pointless zeroing of upper elements. shufps only reads the low element of xmm1 shufps xmm0, xmm1, 0 # The function *should* just comstack to this. ret 

TL: DR: le rest de cette question demande simplement si vous pouvez réellement le faire efficacement, et sinon, pourquoi pas.


L’optimiseur de shuffle de clang obtient ce résultat, et ne gaspille pas les instructions sur la _mm_set_ss(x) à zéro des éléments hauts ( _mm_set_ss(x) ), ou la duplication du scalaire en eux ( _mm_set1_ps(x) ). Au lieu d’écrire quelque chose que le compilateur doit optimiser, ne devrait-il pas y avoir un moyen de l’écrire “efficacement” en C en premier lieu? Même très récent, gcc ne l’ optimise pas, c’est donc un problème réel (mais mineur).


Cela serait possible s’il existait un équivalent scalaire-> 128b de __m256 _mm256_castps128_ps256 (__m128 a) . c’est-à-dire produire un __m128 avec des ordures non définies dans les éléments supérieurs et le flottant dans l’élément bas, en compilant jusqu’à zéro instructions asm si le flottant / double scalaire était déjà dans un registre xmm.

Aucun des éléments insortingnsèques suivants n’existe, mais ils devraient l’être .

  • un scalaire -> __ m128 équivalent à _mm256_castps128_ps256 comme décrit ci-dessus. La solution la plus générale pour le cas scalaire déjà inscrit.
  • __m128 _mm_move_ss_scalar (__m128 a, float s) : remplace l’élément bas du vecteur a par des scalaires s . Ce n’est pas vraiment nécessaire s’il existe un scalaire à usage général -> __ m128 (puce précédente). (La forme de reg-reg de movss fusionne, contrairement à la forme de charge qui zéros, et à la différence de movd qui zéros aux éléments supérieurs dans les deux cas. Pour copier un registre contenant un flottant scalaire sans fausses dépendances, utilisez movaps ).
  • __m128i _mm_loadzxbd (const uint8_t *four_bytes) et d’autres tailles de PMOVZX / PMOVSX: AFAICT, il n’existe pas de méthode sûre permettant d’utiliser les composants insortingnsèques de PMOVZX , car la méthode de sécurité non pratique ne s’optimise pas avec gcc.
  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8) . INSERTPS se comporte différemment comme une charge: les 2 bits supérieurs de l’imm8 sont ignorés et prennent toujours le scalaire à l’adresse effective (au lieu d’un élément d’un vecteur en mémoire). Cela lui permet de travailler avec des adresses qui ne sont pas alignées sur 16B et de fonctionner même sans faute si le float juste avant une page non mappée.

    Comme avec PMOVZX, gcc ne parvient pas à plier un _mm_load_ss() zéro des éléments _mm_load_ss() dans un opérande mémoire pour INSERTPS. (Notez que si les 2 bits supérieurs de l’imm8 ne sont pas tous les deux nuls, alors _mm_insert_ps(xmm0, _mm_load_ss(), imm8) peut comstackr en insertps xmm0,xmm0,foo , avec un imm8 différent qui zéros éléments dans vec as-si l’élément src était en réalité un zéro produit par MOVSS à partir de la mémoire. Clang utilise en fait XORPS / BLENDPS dans ce cas)


Existe-t-il des solutions de contournement viables pour imiter celles qui sont à la fois sûres (ne cassez pas à -O0 en chargeant par exemple 16B qui pourraient toucher la page suivante et segfault), et efficaces (aucune instruction perdue à -O3 avec gcc et clang actuels) au moins, de préférence également d’autres compilateurs majeurs) De préférence également de manière lisible, mais si nécessaire, il peut être placé derrière une fonction d’encapsulage en ligne telle que __m128 float_to_vec(float a){ something(a); } __m128 float_to_vec(float a){ something(a); } .

Existe-t-il une bonne raison pour qu’Intel n’introduise pas de tels éléments insortingnsèques? Ils auraient pu append un float -> __ m128 avec des éléments supérieurs non définis en même temps que _mm256_castps128_ps256 . Est-ce une question d’internes de compilation rendant la mise en œuvre difficile? Peut-être spécifiquement les internes de la CPI?


Les principales conventions d’appel sur x86-64 (SysV ou MS __vectorcall ) prennent le premier argument FP dans xmm0 et renvoient des arguments scalaires FP dans xmm0, avec les éléments supérieurs non définis. (Voir le wiki des balises x86 pour les documents ABI). Cela signifie qu’il n’est pas rare que le compilateur ait un flottant / double scalaire dans un registre avec des éléments supérieurs inconnus. Cela sera rare dans une boucle interne vectorisée, je pense donc qu’en évitant ces instructions inutiles, on économisera surtout un peu de la taille du code.

L’affaire pmovzx est plus grave: vous pouvez l’utiliser dans une boucle interne (par exemple, pour une LUT de masques de lecture aléatoire VPERMD, en économisant un facteur de 4 dans l’empreinte de la mémoire cache par rapport au stockage de chaque index complété à 32 bits en mémoire).


Le problème pmovzx-as-a-load me préoccupe depuis un moment maintenant, et la version originale de cette question m’a amené à réfléchir à la question connexe de l’utilisation d’un flottant scalaire dans un registre xmm. Il y a probablement plus de cas d’utilisation pour pmovzx en tant que charge que pour scalar -> __ m128.

C’est faisable avec GNU C inline asm, mais c’est moche et défait de nombreuses optimisations, y compris la propagation constante ( https://gcc.gnu.org/wiki/DontUseInlineAsm ). Ce ne sera pas la réponse acceptée . J’ajoute cela comme une réponse au lieu d’une partie de la question de sorte que la question rest court n’est pas énorme.

 // don't use this: defeating optimizations is probably worse than an extra instruction #ifdef __GNUC__ __m128 float_to_vec_inlineasm(float x) { __m128 retval; asm ("" : "=x"(retval) : "0"(x)); // matching constraint: provide x in the same xmm reg as retval return retval; } #endif 

Cela comstack en un seul ret , comme vous le souhaitez, et sera en ligne pour vous permettre de shufps un scalaire en un vecteur:

 gcc5.3 float_to_vec_and_shuffle_asm(float __vector(4), float): shufps xmm0, xmm1, 0 # tmp93, xv, ret 

Voir ce code sur l’ explorateur du compilateur Godbolt .

Ceci est évidemment sortingvial en langage assembleur pur, où vous n’avez pas à vous battre avec un compilateur pour le faire ne pas émettre d’instructions inutiles ou inutiles.


Je n’ai pas trouvé de moyen réel d’écrire un __m128 float_to_vec(float a){ something(a); } __m128 float_to_vec(float a){ something(a); } qui comstack juste une instruction ret . Une tentative de double utilisation de _mm_undefined_pd() et _mm_move_sd() rend en réalité un code pire avec gcc (voir le lien Godbolt ci-dessus). Aucun des éléments flottants existants -> __ m128 n’aide.


Stratégies de code _gen_mm_set_ss () réelles : Lorsque vous écrivez du code qui a zéro éléments supérieurs, les compilateurs choisissent parmi une gamme intéressante de stratégies. Certains bons, certains étranges. Les stratégies diffèrent également entre double et float sur le même compilateur (gcc ou clang), comme vous pouvez le voir sur le lien Godbolt ci-dessus.

Un exemple: __m128 float_to_vec(float x){ return _mm_set_ss(x); } __m128 float_to_vec(float x){ return _mm_set_ss(x); } comstack pour:

  # gcc5.3 -march=core2 movd eax, xmm0 # movd xmm0,xmm0 would work; IDK why gcc doesn't do that movd xmm0, eax ret 

  # gcc5.3 -march=nehalem insertps xmm0, xmm0, 0xe ret 

  # clang3.8 -march=nehalem xorps xmm1, xmm1 blendps xmm0, xmm1, 14 # xmm0 = xmm0[0],xmm1[1,2,3] ret