Warum ist meine einfache Quaternion-Multiplikation schneller als die von SSE?

Ich habe einige verschiedene Implementierungen der Quaternion-Multiplikation durchlaufen, war jedoch ziemlich überrascht, dass die Referenzimplementierung bisher meine schnellste ist. Dies ist die fragliche Implementierung:

inline static quat multiply(const quat& lhs, const quat& rhs)
{
    return quat((lhs.w * rhs.x) + (lhs.x * rhs.w) + (lhs.y * rhs.z) - (lhs.z * rhs.y),
                (lhs.w * rhs.y) + (lhs.y * rhs.w) + (lhs.z * rhs.x) - (lhs.x * rhs.z),
                (lhs.w * rhs.z) + (lhs.z * rhs.w) + (lhs.x * rhs.y) - (lhs.y * rhs.x),
                (lhs.w * rhs.w) - (lhs.x * rhs.x) - (lhs.y * rhs.y) - (lhs.z * rhs.z));
}

Ich habe ein paar andere Implementierungen ausprobiert, einige mit SSE, andere nicht. Hier ist ein Beispiel für eine solche SSE-Implementierung, die im Wesentlichen aus der von Bullet Physics verwendeten Bibliothek kopiert wurde:

inline static __m128 multiplynew(__m128 lhs, __m128 rhs)
{
    __m128 qv, tmp0, tmp1, tmp2, tmp3;
    __m128 product, l_wxyz, r_wxyz, xy, qw;
    vec4 sw;

    tmp0 = _mm_shuffle_ps(lhs, lhs, _MM_SHUFFLE(3, 0, 2, 1));
    tmp1 = _mm_shuffle_ps(rhs, rhs, _MM_SHUFFLE(3, 1, 0, 2));
    tmp2 = _mm_shuffle_ps(lhs, lhs, _MM_SHUFFLE(3, 1, 0, 2));
    tmp3 = _mm_shuffle_ps(rhs, rhs, _MM_SHUFFLE(3, 0, 2, 1));
    qv = _mm_mul_ps(_mm_splat_ps(lhs, 3), rhs);
    qv = _mm_madd_ps(_mm_splat_ps(rhs, 3), lhs, qv);
    qv = _mm_madd_ps(tmp0, tmp1, qv);
    qv = _mm_nmsub_ps(tmp2, tmp3, qv);
    product = _mm_mul_ps(lhs, rhs);
    l_wxyz = _mm_sld_ps(lhs, lhs, 12);
    r_wxyz = _mm_sld_ps(rhs, rhs, 12);
    qw = _mm_nmsub_ps(l_wxyz, r_wxyz, product);
    xy = _mm_madd_ps(l_wxyz, r_wxyz, product);
    qw = _mm_sub_ps(qw, _mm_sld_ps(xy, xy, 8));

    sw.uiw = 0xffffffff;
    return _mm_sel_ps(qv, qw, sw);
}

Im Release-Modus mit aktivierten Optimierungen wird meine einfache Referenzimplementierung 70 bis 90% schneller ausgeführt als die SSE-Implementierung von bullet. Im Debug-Modus ohne Optimierungen wird es bis zu dreimal schneller ausgeführt.

Meine erste Frage ist, warum das passiert?

Meine zweite Frage lautet: Gibt es eine Möglichkeit, meine Quaternion-Quaternion-Multiplikationsroutine zu optimieren? Ich möchte mich nicht mit dem Zusammenbau beschäftigen, aber ich verwende sse intrinsics ziemlich oft anderswo.

(Übrigens, wenn es darauf ankommt, ist der Datenspeicher meiner Quaternion definiert alsunion { __m128 data; struct { float x, y, z, w; }; float f[4]; };)

Ich schaute auf die Demontage. Hier ist die Demontage fürmultiply (die schnelle Nonse):

00EC9940  movaps      xmm3,xmmword ptr [esp+0D0h]  
00EC9948  movaps      xmm2,xmmword ptr [esp+0C0h]  
00EC9950  movaps      xmm4,xmm3  
00EC9953  mulss       xmm4,xmm5  
00EC9957  movaps      xmm0,xmm2  
00EC995A  mulss       xmm0,xmm6  
00EC995E  mulss       xmm3,xmm1  
00EC9962  addss       xmm4,xmm0  
00EC9966  movss       xmm0,dword ptr [esp+40h]  
00EC996C  mulss       xmm0,xmm1  
00EC9970  addss       xmm4,xmm0  
00EC9974  movss       xmm0,dword ptr [esp+0F0h]  
00EC997D  mulss       xmm0,xmm7  
00EC9981  subss       xmm4,xmm0  
00EC9985  movss       xmm0,dword ptr [esp+0F0h]  
00EC998E  mulss       xmm0,xmm6  
00EC9992  addss       xmm3,xmm0  
00EC9996  movaps      xmm0,xmm2  
00EC9999  movaps      xmm2,xmmword ptr [esp+40h]  
00EC999E  mulss       xmm0,xmm7  
00EC99A2  addss       xmm3,xmm0  
00EC99A6  movaps      xmm0,xmm2  
00EC99A9  mulss       xmm0,xmm5  
00EC99AD  mulss       xmm2,xmm6  
00EC99B1  subss       xmm3,xmm0  
00EC99B5  movss       xmm0,dword ptr [esp+0D0h]  
00EC99BE  mulss       xmm0,xmm7  
00EC99C2  addss       xmm2,xmm0  
00EC99C6  movss       xmm0,dword ptr [esp+0F0h]  
00EC99CF  mulss       xmm0,xmm5  
00EC99D3  addss       xmm2,xmm0  
00EC99D7  movss       xmm0,dword ptr [esp+0C0h]  
00EC99E0  mulss       xmm0,xmm1  
00EC99E4  movss       xmm1,dword ptr [esp+0D0h]  
00EC99ED  mulss       xmm1,xmm6  
00EC99F1  subss       xmm2,xmm0  
00EC99F5  movss       xmm0,dword ptr [esp+0C0h]  
00EC99FE  mulss       xmm0,xmm5  
00EC9A02  movaps      xmm5,xmmword ptr [esp+50h]  
00EC9A07  unpcklps    xmm4,xmm2  
00EC9A0A  subss       xmm1,xmm0  
00EC9A0E  movss       xmm0,dword ptr [esp+0F0h]  
00EC9A17  mulss       xmm0,xmm5  
00EC9A1B  subss       xmm1,xmm0  
00EC9A1F  movss       xmm0,dword ptr [esp+40h]  
00EC9A25  mulss       xmm0,xmm7  
00EC9A29  subss       xmm1,xmm0  
00EC9A2D  unpcklps    xmm3,xmm1  
00EC9A30  unpcklps    xmm4,xmm3  
00EC9A33  movaps      xmm5,xmm4  
00EC9A36  movaps      xmmword ptr [esp+30h],xmm5  
00EC9A3B  dec         eax  
00EC9A3C  je          SDL_main+58Ah (0EC9A5Ah)  

Und hier ist die Demontage fürmultiplynew (die langsame):

00329BF3  movaps      xmm6,xmm5  
00329BF6  mulps       xmm6,xmm1  
00329BF9  movaps      xmm0,xmm5  
00329BFC  mov         dword ptr [esp+6Ch],0FFFFFFFFh  
00329C04  shufps      xmm0,xmm5,93h  
00329C08  movaps      xmm1,xmm5  
00329C0B  mulps       xmm4,xmm0  
00329C0E  movaps      xmm0,xmmword ptr [esp+110h]  
00329C16  movaps      xmm3,xmm6  
00329C19  shufps      xmm1,xmm5,0FFh  
00329C1D  mulps       xmm1,xmmword ptr [esp+40h]  
00329C22  movaps      xmm7,xmmword ptr [esp+60h]  
00329C27  addps       xmm3,xmm4  
00329C2A  mulps       xmm0,xmm5  
00329C2D  subps       xmm6,xmm4  
00329C30  shufps      xmm3,xmm3,4Eh  
00329C34  addps       xmm1,xmm0  
00329C37  movaps      xmm0,xmm5  
00329C3A  shufps      xmm0,xmm5,0C9h  
00329C3E  subps       xmm6,xmm3  
00329C41  mulps       xmm0,xmmword ptr [esp+120h]  
00329C49  shufps      xmm5,xmm5,0D2h  
00329C4D  mulps       xmm5,xmmword ptr [esp+0C0h]  
00329C55  andps       xmm6,xmmword ptr [esp+60h]  
00329C5A  addps       xmm1,xmm0  
00329C5D  subps       xmm1,xmm5  
00329C60  andnps      xmm7,xmm1  

So teste ich die Geschwindigkeit:

timer.update();
for (uint i = 0; i < 1000000; ++i)
{
    temp1 = quat::multiply(temp1, q1);
}
timer.update();
printf("1M calls to multiplyOld took %fs.\n", timer.getDeltaTime());

(timer.getDeltaTime () gibt in Sekunden an, wie viel Zeit zwischen dem letzten Aufruf von timer.update () und dem vorherigen Aufruf von timer.update () vergangen ist.)

Warum läuft meine non-sse Version trotz mehr Anweisungen schneller? Lese ich die Demontage falsch oder so?

BEARBEITEN: Ich habe festgestellt, dass die sse-Version beim Kompilieren in x64 schneller ausgeführt wird als die Nicht-sse-Version.

Antworten auf die Frage(2)

Ihre Antwort auf die Frage