¿Cómo combinar un escalar en un vector sin que el comstackdor pierda una instrucción poniendo a cero los elementos superiores? ¿Limitación de diseño en los intrínsecos de Intel?

No tengo un caso de uso particular en mente; Pregunto si esto es realmente una falla / limitación de diseño en los intrínsecos de Intel o si me falta algo.

Si desea combinar un flotante escalar con un vector existente, no parece haber una manera de hacerlo sin un alto cero de elementos o transmitir el escalar en un vector, usando los intrínsecos de Intel. No he investigado las extensiones vectoriales nativas de GNU C y los builtins asociados.

Esto no sería tan malo si el extra intrínseco se optimiza, pero no con gcc (5.4 o 6.2). Tampoco hay una buena manera de usar pmovzx o insertps como cargas, por la razón relacionada de que sus intrínsecos solo toman args vectoriales. (Y gcc no dobla una carga escalar-> vector en la instrucción asm).

 __m128 replace_lower_two_elements(__m128 v, float x) { __m128 xv = _mm_set_ss(x); // WANTED: something else for this step, some compilers actually compile 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 salida, para habilitar SSE4.1 y sintonizar esa CPU Intel: (Es aún peor sin SSE4.1; instrucciones múltiples para poner a cero los elementos superiores).

  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 compile to this. ret 

TL: DR: el rest de esta pregunta es solo preguntarte si realmente puedes hacer esto de manera eficiente, y si no, por qué no.


El shuffle-optimizer de clang hace esto bien, y no desperdicia instrucciones para poner a cero elementos altos ( _mm_set_ss(x) ), o duplicar el escalar en ellos ( _mm_set1_ps(x) ). En lugar de escribir algo que el comstackdor tiene que optimizar, ¿no debería haber una manera de escribirlo “eficientemente” en C en primer lugar? Incluso un gcc muy reciente no lo optimiza, así que este es un problema real (pero menor).


Esto sería posible si hubiera un escalar-> 128b equivalente a __m256 _mm256_castps128_ps256 (__m128 a) . es decir, produce un __m128 con basura indefinida en los elementos superiores, y el flotante en el elemento bajo, comstackndo instrucciones de cero asm si el flotador / doble escalar ya estaba en un registro xmm.

Ninguno de los siguientes intrínsecos existe, pero deberían .

  • un escalar -> __ m128 equivalente a _mm256_castps128_ps256 como se describió anteriormente. La solución más general para el caso escalar ya en registro.
  • __m128 _mm_move_ss_scalar (__m128 a, float s) : reemplaza el elemento bajo del vector a con escalar s . Esto no es realmente necesario si hay un escalar de propósito general -> __ m128 (viñeta anterior). (La forma reg-reg de movss fusiona, a diferencia de la forma de carga que ceros, y a diferencia de movd que movd los elementos superiores en ambos casos. Para copiar un registro que contiene un flotante escalar sin dependencias falsas, utilice movaps ).
  • __m128i _mm_loadzxbd (const uint8_t *four_bytes) y otros tamaños de PMOVZX / PMOVSX: AFAICT, no hay una buena manera segura de utilizar los intrínsecos de PMOVZX como carga , ya que la forma inconveniente y segura no se optimiza con gcc.
  • __m128 _mm_insertload_ps (__m128 a, float *s, const int imm8) . INSERTPS se comporta de forma diferente como carga: los 2 bits superiores de imm8 se ignoran, y siempre toma el escalar en la dirección efectiva (en lugar de un elemento de un vector en la memoria). Esto le permite trabajar con direcciones que no están alineadas con 16B, y funcionan incluso sin fallas si el float justo antes de una página no mapeada.

    Al igual que con PMOVZX, gcc no puede plegar un elemento superior-zeroing _mm_load_ss() en un operando de memoria para INSERTPS. (Tenga en cuenta que si los 2 bits superiores de imm8 no son ambos cero, entonces _mm_insert_ps(xmm0, _mm_load_ss(), imm8) puede comstackr para insertps xmm0,xmm0,foo , con un imm8 diferente que pone ceros a los elementos en vec as-if el elemento src era en realidad un cero producido por MOVSS desde la memoria. Clang realmente usa XORPS / BLENDPS en ese caso)


¿Hay soluciones alternativas viables para emular cualquiera de las que son seguras (no se rompa en -00 por ejemplo cargando 16B que podría tocar la página siguiente y segfault), y eficiente (no hay instrucciones desperdiciadas en -O3 con gcc actual y clang al menos, preferiblemente también otros comstackdores principales)? Preferiblemente también de una manera legible, pero si es necesario, podría ponerse detrás de una función de envoltura en línea como __m128 float_to_vec(float a){ something(a); } __m128 float_to_vec(float a){ something(a); } .

¿Hay alguna buena razón para que Intel no introduzca intrínsecos como ese? Podrían haber agregado un flotador -> __ m128 con elementos superiores indefinidos al mismo tiempo que se agrega _mm256_castps128_ps256 . ¿Se trata de una cuestión interna del comstackdor que dificulta su implementación? Tal vez específicamente en el interior de ICC?


Las principales convenciones de llamadas en x86-64 (SysV o MS __vectorcall ) toman el primer FP arg en xmm0 y devuelven los argumentos escalares FP en xmm0, con los elementos superiores indefinidos. (Consulte la wiki de tags x86 para documentos de ABI). Esto significa que no es raro que el comstackdor tenga un flotante / doble escalar en un registro con elementos superiores desconocidos. Esto será raro en un bucle interno vectorizado, por lo que creo que evitar estas instrucciones inútiles en su mayoría solo ahorrará un poco del tamaño del código.

El caso pmovzx es más serio: eso es algo que podrías usar en un bucle interno (por ejemplo, para una LUT de máscaras de mezcla de VPERMD, guardando un factor de 4 en la huella de caché versus almacenando cada índice rellenado a 32 bits en la memoria).


El problema de pmovzx-como-carga me ha estado molestando por un tiempo, y la versión original de esta pregunta me hizo pensar sobre el tema relacionado de usar un flotador escalar en un registro xmm. Probablemente haya más casos de uso para pmovzx como carga que para escalar -> __ m128.

Es factible con GNU C inline asm, pero esto es feo y derrota muchas optimizaciones, incluida la propagación constante ( https://gcc.gnu.org/wiki/DontUseInlineAsm ). Esta no será la respuesta aceptada . Estoy agregando esto como una respuesta en lugar de una parte de la pregunta, por lo que la pregunta se queda corto no es enorme

 // 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 

Esto se comstack a un solo ret , como se desee, y se shufps para permitirle shufps un escalar en un vector:

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

Vea este código en el explorador del comstackdor Godbolt .

Esto es obviamente trivial en lenguaje ensamblador puro, donde no tienes que pelear con un comstackdor para que no emita instrucciones que no quieres o no necesitas.


No he encontrado ninguna forma real de escribir un __m128 float_to_vec(float a){ something(a); } __m128 float_to_vec(float a){ something(a); } que comstack a solo una instrucción ret . Un bash de double usando _mm_undefined_pd() y _mm_move_sd() realidad empeora el código con gcc (ver el enlace de Godbolt arriba). Ninguno de los flotantes existentes -> __ m128 intrínsecos ayudan.


Fuera del tema: estrategias de código gen geniales _mm_set_ss () : Cuando se escribe un código que tiene que poner a cero los elementos superiores, los comstackdores eligen entre un rango interesante de estrategias. Algunas buenas, otras raras. Las estrategias también difieren entre double y float en el mismo comstackdor (gcc o clang), como se puede ver en el enlace Godbolt arriba.

Un ejemplo: __m128 float_to_vec(float x){ return _mm_set_ss(x); } __m128 float_to_vec(float x){ return _mm_set_ss(x); } comstack a:

  # 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