c++ assembly optimization x86 sse

c++ - Rendimiento de tipo de envoltura de vector SSE en comparación con desnudo__m128



assembly optimization (1)

Como resultó, el problema no está en la struct Vec4 definida por el usuario struct Vec4 . Está profundamente relacionado con las convenciones de llamadas x86.

La convención de llamadas x86 predeterminada en Visual C ++ es __cdecl , que

Impulsa los parámetros en la pila, en orden inverso (de derecha a izquierda)

Ahora esto es un problema, ya que Vec4 debería mantenerse y pasar en un registro XMM. Pero veamos qué está sucediendo realmente.

Primer caso

En el primer caso, Vec4 es un alias de tipo simple de __m128 .

using Vec4 = __m128; /* ... */ Vec4 TestEQ(EQSTATE* es, Vec4 &sample) { ... }

El encabezado de la función generada de TestEQ en el ensamblaje es

?TestEQ@@YA?AT__m128@@PAUEQSTATE@@AAT1@@Z PROC ; TestEQ, COMDAT ; _es$ = ecx ; _sample$ = edx ...

Bonito.

2do caso

En el segundo caso, Vec4 no es un alias de __m128 , ahora es un tipo definido por el usuario.

Aquí investigo la compilación para plataformas x86 y x64.

x86 (compilación de 32 bits)

Como __cdecl (que es la convención de llamadas predeterminada en x86) no permite pasar valores alineados a funciones (que emitirían Error C2719: ''sample'': formal parameter with requested alignment of 16 won''t be aligned ) lo pasamos por const referencia.

struct Vec4{ __m128 simd; /* ... */ }; /* ... */ Vec4 TestEQ(EQSTATE* es, const Vec4 &sample) { ... }

que genera el encabezado de función para TestEQ como

?TestEQ@@YA?AUVec4@@PAUEQSTATE@@ABU1@@Z PROC ; TestEQ, COMDAT ; ___$ReturnUdt$ = ecx ; _es$ = edx push ebx mov ebx, esp sub esp, 8 and esp, -8 ; fffffff8H add esp, 4 push ebp mov ebp, DWORD PTR [ebx+4] mov eax, DWORD PTR _sample$[ebx] ...

Esto no es tan simple como el del primer caso. Los argumentos se mueven a la pila. Hay algunas instrucciones mov adicionales entre las primeras instrucciones de SSE también, que no se enumeran aquí. Estas instrucciones en general son suficientes para afectar un poco el rendimiento.

x64 (compilación de 64 bits)

Windows en x64 utiliza una convención de llamadas diferente como parte de la Interfaz binaria de aplicación x64 (ABI) .

Esta convención trata de mantener los datos en registros si es posible, de una manera que los datos de punto flotante se mantengan en registros XMM.

Desde MSDN Descripción general de las convenciones de llamadas x64 :

La interfaz binaria de aplicaciones x64 (ABI) es una convención de llamadas de registro rápido de 4 registros, con respaldo de pila para esos registros. Existe una correspondencia estricta de uno a uno entre los argumentos de una función y los registros de esos argumentos. Cualquier argumento que no encaje en 8 bytes, o que no sea 1, 2, 4 u 8 bytes, se debe pasar por referencia. (...) Todas las operaciones de punto flotante se realizan utilizando los 16 registros XMM. Los argumentos se pasan en los registros RCX, RDX, R8 y R9. Si los argumentos son float / double, se pasan en XMM0L, XMM1L, XMM2L y XMM3L. Los argumentos de 16 bytes se pasan por referencia.

Desde la página de Wikipedia para las convenciones de llamadas x86-64

Se sigue la convención de llamadas de Microsoft x64 en Windows y UEFI previo al arranque (para modo largo en x86-64). Utiliza los registros RCX, RDX, R8, R9 para los primeros cuatro argumentos enteros o de puntero (en ese orden), y XMM0, XMM1, XMM2, XMM3 se utilizan para argumentos de coma flotante. Se insertan argumentos adicionales en la pila (de derecha a izquierda). Los valores de retorno enteros (similar a x86) se devuelven en RAX si son de 64 bits o menos. Los valores de retorno de punto flotante se devuelven en XMM0.

Entonces, el segundo caso en modo x64 genera el encabezado de función para TestEQ como

?TestEQ@@YQ?AUVec4@@PAUEQSTATE@@ABU1@@Z PROC ; TestEQ, COMDAT ; _es$ = ecx ; _sample$ = edx ...

¡Esto es exactamente lo mismo que el primer caso!

Solución

Para el modo x86, el comportamiento presentado debe ser claramente corregido.

La solución más simple es inline la función. Aunque esto es solo una pista y el compilador puede ignorar por completo, puede decirle al compilador que siempre alinee la función. Sin embargo, a veces esto no se desea debido al tamaño de la función o por cualquier otro motivo.

Afortunadamente, Microsoft introdujo la convención __vectorcall en Visual Studio 2013 y superior (disponible tanto en modo x86 como x64). Esto es muy similar a la convención de llamadas predeterminada de Windows x64, pero con más registros utilizables.

Vamos a reescribir el segundo caso con __vectorcall :

Vec4 __vectorcall TestEQ(EQSTATE* es, const Vec4 &sample) { ... }

Ahora el encabezado de la función de ensamblaje generado para TestEQ es

?TestEQ@@YQ?AUVec4@@PAUEQSTATE@@ABU1@@Z PROC ; TestEQ, COMDAT ; _es$ = ecx ; _sample$ = edx ...

que finalmente es lo mismo que el primer caso y el segundo caso en x64 .

Como señaló Peter Cordes, para aprovechar al máximo __vectorcall , el argumento Vec4 debería pasarse por valor, en lugar de referencia constante. Para hacer esto, el tipo pasado debe cumplir con algunos requisitos, como debe ser copiable trivialmente construible (sin constructores de copia definidos por el usuario) y no debe contener ninguna unión. Más información en los comentarios a continuación y here .

Ultimas palabras

Parece que MSVC bajo el capó aplica automáticamente la convención __vectorcall como una optimización cuando detecta un argumento __m128 . De lo contrario, utiliza la convención de llamada predeterminada __cdecl (puede cambiar este comportamiento por las opciones del compilador).

La gente me dijo en los comentarios que no veían mucha diferencia entre la asamblea generada por GCC y Clang de los dos casos. Esto se debe a que estos compiladores con indicador de optimización -O2 simplemente TestEQ función TestEQ en el cuerpo del bucle de prueba ( https://godbolt.org/g/fZ8X0N ). También es posible que sean más inteligentes que MSVC y realizarían una mejor optimización de la llamada de función.

Encontré un artículo interesante de Gamasutra sobre las trampas de SIMD, que afirma que no es posible alcanzar el rendimiento del tipo "puro" __m128 con tipos de envoltura. Bueno, era escéptico, así que descargué los archivos del proyecto y fabricé un caso de prueba comparable.

Resultó (para mi sorpresa) que la versión del contenedor es significativamente más lenta. Como no quiero hablar solo del aire, los casos de prueba son los siguientes:

En el primer caso, Vec4 es un alias simple del tipo __m128 con algunos operadores:

#include <xmmintrin.h> #include <emmintrin.h> using Vec4 = __m128; inline __m128 VLoad(float f) { return _mm_set_ps(f, f, f, f); }; inline Vec4& operator+=(Vec4 &va, Vec4 vb) { return (va = _mm_add_ps(va, vb)); }; inline Vec4& operator*=(Vec4 &va, Vec4 vb) { return (va = _mm_mul_ps(va, vb)); }; inline Vec4 operator+(Vec4 va, Vec4 vb) { return _mm_add_ps(va, vb); }; inline Vec4 operator-(Vec4 va, Vec4 vb) { return _mm_sub_ps(va, vb); }; inline Vec4 operator*(Vec4 va, Vec4 vb) { return _mm_mul_ps(va, vb); };

En el segundo caso, Vec4 es un contenedor liviano de alrededor de __m128 . No es un envoltorio completo, solo un breve boceto que cubre el problema. Los operadores envuelven exactamente los mismos intrínsecos, la única diferencia es (ya que la alineación de 16 bytes no se puede aplicar en los argumentos) que toman Vec4 como referencia const .

#include <xmmintrin.h> #include <emmintrin.h> struct Vec4 { __m128 simd; inline Vec4() = default; inline Vec4(const Vec4&) = default; inline Vec4& operator=(const Vec4&) = default; inline Vec4(__m128 s) : simd(s) {} inline operator __m128() const { return simd; } inline operator __m128&() { return simd; } }; inline __m128 VLoad(float f) { return _mm_set_ps(f, f, f, f); }; inline Vec4 VAdd(const Vec4 &va, const Vec4 &vb) { return _mm_add_ps(va, vb); // return _mm_add_ps(va.simd, vb.simd); // doesn''t make difference }; inline Vec4 VSub(const Vec4 &va, const Vec4 &vb) { return _mm_sub_ps(va, vb); // return _mm_sub_ps(va.simd, vb.simd); // doesn''t make difference }; inline Vec4 VMul(const Vec4 &va, const Vec4 &vb) { return _mm_mul_ps(va, vb); // return _mm_mul_ps(va.simd, vb.simd); // doesn''t make difference };

Y aquí está el kernel de prueba que produce un rendimiento diferente con diferentes versiones de Vec4 :

#include <xmmintrin.h> #include <emmintrin.h> struct EQSTATE { // Filter #1 (Low band) Vec4 lf; // Frequency Vec4 f1p0; // Poles ... Vec4 f1p1; Vec4 f1p2; Vec4 f1p3; // Filter #2 (High band) Vec4 hf; // Frequency Vec4 f2p0; // Poles ... Vec4 f2p1; Vec4 f2p2; Vec4 f2p3; // Sample history buffer Vec4 sdm1; // Sample data minus 1 Vec4 sdm2; // 2 Vec4 sdm3; // 3 // Gain Controls Vec4 lg; // low gain Vec4 mg; // mid gain Vec4 hg; // high gain }; static float vsaf = (1.0f / 4294967295.0f); // Very small amount (Denormal Fix) static Vec4 vsa = VLoad(vsaf); Vec4 TestEQ(EQSTATE* es, Vec4& sample) { // Locals Vec4 l,m,h; // Low / Mid / High - Sample Values // Filter #1 (lowpass) es->f1p0 += (es->lf * (sample - es->f1p0)) + vsa; //es->f1p0 = VAdd(es->f1p0, VAdd(VMul(es->lf, VSub(sample, es->f1p0)), vsa)); es->f1p1 += (es->lf * (es->f1p0 - es->f1p1)); //es->f1p1 = VAdd(es->f1p1, VMul(es->lf, VSub(es->f1p0, es->f1p1))); es->f1p2 += (es->lf * (es->f1p1 - es->f1p2)); //es->f1p2 = VAdd(es->f1p2, VMul(es->lf, VSub(es->f1p1, es->f1p2))); es->f1p3 += (es->lf * (es->f1p2 - es->f1p3)); //es->f1p3 = VAdd(es->f1p3, VMul(es->lf, VSub(es->f1p2, es->f1p3))); l = es->f1p3; // Filter #2 (highpass) es->f2p0 += (es->hf * (sample - es->f2p0)) + vsa; //es->f2p0 = VAdd(es->f2p0, VAdd(VMul(es->hf, VSub(sample, es->f2p0)), vsa)); es->f2p1 += (es->hf * (es->f2p0 - es->f2p1)); //es->f2p1 = VAdd(es->f2p1, VMul(es->hf, VSub(es->f2p0, es->f2p1))); es->f2p2 += (es->hf * (es->f2p1 - es->f2p2)); //es->f2p2 = VAdd(es->f2p2, VMul(es->hf, VSub(es->f2p1, es->f2p2))); es->f2p3 += (es->hf * (es->f2p2 - es->f2p3)); //es->f2p3 = VAdd(es->f2p3, VMul(es->hf, VSub(es->f2p2, es->f2p3))); h = es->sdm3 - es->f2p3; //h = VSub(es->sdm3, es->f2p3); // Calculate midrange (signal - (low + high)) m = es->sdm3 - (h + l); //m = VSub(es->sdm3, VAdd(h, l)); // Scale, Combine and store l *= es->lg; m *= es->mg; h *= es->hg; //l = VMul(l, es->lg); //m = VMul(m, es->mg); //h = VMul(h, es->hg); // Shuffle history buffer es->sdm3 = es->sdm2; es->sdm2 = es->sdm1; es->sdm1 = sample; // Return result return(l + m + h); //return(VAdd(l, VAdd(m, h))); } //make these as globals to enforce the function call; static Vec4 sample[1024], result[1024]; static EQSTATE es; #include <chrono> #include <iostream> int main() { auto t0 = std::chrono::high_resolution_clock::now(); for (int ii=0; ii<1024; ii++) { result[ii] = TestEQ(&es, sample[ii]); } auto t1 = std::chrono::high_resolution_clock::now(); auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(t1 - t0).count(); std::cout << "timing: " << t << ''/n''; std::cin.get(); return 0; }

Enlace al código de trabajo

MSVC 2015 generado conjunto para la primera versión :

; COMDAT ?TestEQ@@YA?AT__m128@@PAUEQSTATE@@AAT1@@Z _TEXT SEGMENT ?TestEQ@@YA?AT__m128@@PAUEQSTATE@@AAT1@@Z PROC ; TestEQ, COMDAT ; _es$dead$ = ecx ; _sample$ = edx vmovaps xmm0, XMMWORD PTR [edx] vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+16 vmovaps xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?vsa@@3T__m128@@A vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+16 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+16, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+32 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+32 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+32, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+48 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+48 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+48, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+64 vmulps xmm0, xmm0, xmm2 vaddps xmm4, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+64 vmovaps xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A+80 vmovaps xmm1, XMMWORD PTR ?es@@3UEQSTATE@@A+192 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+64, xmm4 vmovaps xmm0, XMMWORD PTR [edx] vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+96 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?vsa@@3T__m128@@A vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+96 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+96, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+112 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+112 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+112, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+128 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+128 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+128, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+144 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+144 vsubps xmm2, xmm1, xmm0 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+144, xmm0 vmovaps xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+176 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+192, xmm0 vmovaps xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+160 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+176, xmm0 vmovaps xmm0, XMMWORD PTR [edx] vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+160, xmm0 vaddps xmm0, xmm4, xmm2 vsubps xmm0, xmm1, xmm0 vmulps xmm1, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+224 vmulps xmm0, xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A+240 vaddps xmm1, xmm1, xmm0 vmulps xmm0, xmm4, XMMWORD PTR ?es@@3UEQSTATE@@A+208 vaddps xmm0, xmm1, xmm0 ret 0 ?TestEQ@@YA?AT__m128@@PAUEQSTATE@@AAT1@@Z ENDP ; TestEQ

MSVC 2015 generado conjunto para la segunda versión :

?TestEQ@@YA?AUVec4@VMATH@@PAUEQSTATE@@AAU12@@Z PROC ; TestEQ, COMDAT ; ___$ReturnUdt$ = ecx ; _es$dead$ = edx push ebx mov ebx, esp sub esp, 8 and esp, -8 ; fffffff8H add esp, 4 push ebp mov ebp, DWORD PTR [ebx+4] mov eax, DWORD PTR _sample$[ebx] vmovaps xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A vmovaps xmm1, XMMWORD PTR ?es@@3UEQSTATE@@A+192 mov DWORD PTR [esp+4], ebp vmovaps xmm0, XMMWORD PTR [eax] vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+16 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?vsa@@3UVec4@VMATH@@A vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+16 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+16, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+32 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+32 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+32, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+48 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+48 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+48, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+64 vmulps xmm0, xmm0, xmm2 vaddps xmm4, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+64 vmovaps xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A+80 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+64, xmm4 vmovaps xmm0, XMMWORD PTR [eax] vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+96 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?vsa@@3UVec4@VMATH@@A vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+96 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+96, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+112 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+112 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+112, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+128 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+128 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+128, xmm0 vsubps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+144 vmulps xmm0, xmm0, xmm2 vaddps xmm0, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+144 vsubps xmm2, xmm1, xmm0 vmovaps XMMWORD PTR ?es@@3UEQSTATE@@A+144, xmm0 vaddps xmm0, xmm2, xmm4 vsubps xmm0, xmm1, xmm0 vmulps xmm1, xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+224 vmovdqu xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+176 vmovdqu XMMWORD PTR ?es@@3UEQSTATE@@A+192, xmm0 vmovdqu xmm0, XMMWORD PTR ?es@@3UEQSTATE@@A+160 vmovdqu XMMWORD PTR ?es@@3UEQSTATE@@A+176, xmm0 vmovdqu xmm0, XMMWORD PTR [eax] vmovdqu XMMWORD PTR ?es@@3UEQSTATE@@A+160, xmm0 vmulps xmm0, xmm4, XMMWORD PTR ?es@@3UEQSTATE@@A+208 vaddps xmm1, xmm0, xmm1 vmulps xmm0, xmm2, XMMWORD PTR ?es@@3UEQSTATE@@A+240 vaddps xmm0, xmm1, xmm0 vmovaps XMMWORD PTR [ecx], xmm0 mov eax, ecx pop ebp mov esp, ebx pop ebx ret 0 ?TestEQ@@YA?AUVec4@VMATH@@PAUEQSTATE@@AAU12@@Z ENDP ; TestEQ

El ensamblaje producido de la segunda versión es significativamente más largo y más lento. No está estrictamente relacionado con Visual Studio , ya que Clang 3.8 produce resultados de rendimiento similares.

Clang 3.8 generado conjunto para la primera versión :

"?TestEQ@@YAT__m128@@PAUEQSTATE@@AAT1@@Z": # @"/01?TestEQ@@YAT__m128@@PAUEQSTATE@@AAT1@@Z" Lfunc_begin0: Ltmp0: # BB#0: # %entry movl 8(%esp), %eax movl 4(%esp), %ecx vmovaps _vsa, %xmm0 vmovaps (%ecx), %xmm1 vmovaps 16(%ecx), %xmm2 vmovaps (%eax), %xmm3 vsubps %xmm2, %xmm3, %xmm3 vmulps %xmm3, %xmm1, %xmm3 vaddps %xmm3, %xmm0, %xmm3 vaddps %xmm3, %xmm2, %xmm2 vmovaps %xmm2, 16(%ecx) vmovaps 32(%ecx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm2, %xmm1, %xmm2 vaddps %xmm2, %xmm3, %xmm2 vmovaps %xmm2, 32(%ecx) vmovaps 48(%ecx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm2, %xmm1, %xmm2 vaddps %xmm2, %xmm3, %xmm2 vmovaps %xmm2, 48(%ecx) vmovaps 64(%ecx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm2, %xmm1, %xmm1 vaddps %xmm1, %xmm3, %xmm1 vmovaps %xmm1, 64(%ecx) vmovaps 80(%ecx), %xmm2 vmovaps 96(%ecx), %xmm3 vmovaps (%eax), %xmm4 vsubps %xmm3, %xmm4, %xmm4 vmulps %xmm4, %xmm2, %xmm4 vaddps %xmm4, %xmm0, %xmm0 vaddps %xmm0, %xmm3, %xmm0 vmovaps %xmm0, 96(%ecx) vmovaps 112(%ecx), %xmm3 vsubps %xmm3, %xmm0, %xmm0 vmulps %xmm0, %xmm2, %xmm0 vaddps %xmm0, %xmm3, %xmm0 vmovaps %xmm0, 112(%ecx) vmovaps 128(%ecx), %xmm3 vsubps %xmm3, %xmm0, %xmm0 vmulps %xmm0, %xmm2, %xmm0 vaddps %xmm0, %xmm3, %xmm0 vmovaps %xmm0, 128(%ecx) vmovaps 144(%ecx), %xmm3 vsubps %xmm3, %xmm0, %xmm0 vmulps %xmm0, %xmm2, %xmm0 vaddps %xmm0, %xmm3, %xmm0 vmovaps %xmm0, 144(%ecx) vmovaps 192(%ecx), %xmm2 vsubps %xmm0, %xmm2, %xmm0 vaddps %xmm0, %xmm1, %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps 208(%ecx), %xmm1, %xmm1 vmulps 224(%ecx), %xmm2, %xmm2 vmulps 240(%ecx), %xmm0, %xmm0 vmovaps 176(%ecx), %xmm3 vmovaps %xmm3, 192(%ecx) vmovaps 160(%ecx), %xmm3 vmovaps %xmm3, 176(%ecx) vmovaps (%eax), %xmm3 vmovaps %xmm3, 160(%ecx) vaddps %xmm2, %xmm0, %xmm0 vaddps %xmm0, %xmm1, %xmm0 retl Lfunc_end0:

Clang 3.8 generado conjunto para la segunda versión :

"?TestEQ@@YA?AUVec4@@PAUEQSTATE@@AAU1@@Z": # @"/01?TestEQ@@YA?AUVec4@@PAUEQSTATE@@AAU1@@Z" Lfunc_begin0: Ltmp0: # BB#0: # %entry movl 12(%esp), %ecx movl 8(%esp), %edx vmovaps (%edx), %xmm0 vmovaps 16(%edx), %xmm1 vmovaps (%ecx), %xmm2 vsubps %xmm1, %xmm2, %xmm2 vmulps %xmm0, %xmm2, %xmm2 vaddps _vsa, %xmm2, %xmm2 vaddps %xmm2, %xmm1, %xmm1 vmovaps %xmm1, 16(%edx) vmovaps 32(%edx), %xmm2 vsubps %xmm2, %xmm1, %xmm1 vmulps %xmm0, %xmm1, %xmm1 vaddps %xmm1, %xmm2, %xmm1 vmovaps %xmm1, 32(%edx) vmovaps 48(%edx), %xmm2 vsubps %xmm2, %xmm1, %xmm1 vmulps %xmm0, %xmm1, %xmm1 vaddps %xmm1, %xmm2, %xmm1 vmovaps %xmm1, 48(%edx) vmovaps 64(%edx), %xmm2 vsubps %xmm2, %xmm1, %xmm1 vmulps %xmm0, %xmm1, %xmm0 vaddps %xmm0, %xmm2, %xmm0 vmovaps %xmm0, 64(%edx) vmovaps 80(%edx), %xmm1 vmovaps 96(%edx), %xmm2 vmovaps (%ecx), %xmm3 vsubps %xmm2, %xmm3, %xmm3 vmulps %xmm1, %xmm3, %xmm3 vaddps _vsa, %xmm3, %xmm3 vaddps %xmm3, %xmm2, %xmm2 vmovaps %xmm2, 96(%edx) vmovaps 112(%edx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm1, %xmm2, %xmm2 vaddps %xmm2, %xmm3, %xmm2 vmovaps %xmm2, 112(%edx) vmovaps 128(%edx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm1, %xmm2, %xmm2 vaddps %xmm2, %xmm3, %xmm2 vmovaps %xmm2, 128(%edx) vmovaps 144(%edx), %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps %xmm1, %xmm2, %xmm1 vaddps %xmm1, %xmm3, %xmm1 vmovaps %xmm1, 144(%edx) vmovaps 192(%edx), %xmm2 vsubps %xmm1, %xmm2, %xmm1 vaddps %xmm1, %xmm0, %xmm3 vsubps %xmm3, %xmm2, %xmm2 vmulps 208(%edx), %xmm0, %xmm0 vmulps 224(%edx), %xmm2, %xmm2 movl 4(%esp), %eax vmulps 240(%edx), %xmm1, %xmm1 vmovaps 176(%edx), %xmm3 vmovaps %xmm3, 192(%edx) vmovaps 160(%edx), %xmm3 vmovaps %xmm3, 176(%edx) vmovaps (%ecx), %xmm3 vmovaps %xmm3, 160(%edx) vaddps %xmm2, %xmm0, %xmm0 vaddps %xmm0, %xmm1, %xmm0 vmovaps %xmm0, (%eax) retl Lfunc_end0:

Aunque el número de instrucciones es el mismo, la primera versión es todavía un 50% más rápida.

Traté de identificar la causa del problema, sin éxito. Hay cosas sospechosas como esas feas instrucciones de vmovdqu en el segundo ensamblado de MSVC. El operador de construcción, asignación de copias y pass-by-reference también pueden mover innecesariamente los datos de los registros SSE a la memoria, sin embargo, todos mis intentos de resolver o identificar exactamente el problema no tuvieron éxito.

Realmente no creo que un envoltorio tan simple no pueda alcanzar el mismo rendimiento que el desnudo __m128 , cualquiera que sea la causa de la sobrecarga podría ser eliminado.

Entonces, ¿qué está pasando allí?