【问题标题】:Why is my straightforward quaternion multiplication faster than SSE?为什么我的直接四元数乘法比 SSE 快?
【发布时间】:2014-03-06 04:47:41
【问题描述】:

我已经经历了几个不同的四元数乘法实现,但看到参考实现是迄今为止我最快的,我感到相当惊讶。这是有问题的实现:

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));
}

我尝试了一些其他实现,一些使用 SSE,一些不使用。这是一个此类 SSE 实现的示例,基本上是从 Bullet Physics 使用的库中复制的:

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);
}

在启用优化的发布模式下,我的简单参考实现比子弹的 SSE 实现快 70%-90%。在没有优化的调试模式下,它的运行速度提高了 3 倍。

我的第一个问题是,为什么会这样?

我的第二个问题是,有什么方法可以优化我的四元数-四元数乘法程序吗?我不想处理汇编,但我在其他地方经常使用 sse 内在函数。

(顺便说一句,如果重要的话,我的四元数的数据存储定义为union { __m128 data; struct { float x, y, z, w; }; float f[4]; };

我看了下拆解。这是multiply(快速非sse)的反汇编:

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)  

这是multiplynew 的反汇编(sse 慢):

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  

我测试速度的方式是使用:

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() 返回上次调用 timer.update() 和之前调用 timer.update() 之间的时间间隔,以秒为单位。)

为什么我的非 sse 版本尽管有更多指令但运行得更快..?我是不是把反汇编读错了?

编辑:当我在 x64 中编译时,我发现 sse 版本的运行速度比非 sse 版本快。

【问题讨论】:

  • 能否请您发布每个版本的发布版本的反汇编?为了解释为什么一个会比另一个更快,反汇编将更接近 CPU 所看到的。
  • 我也想问一下 CPU 信息,最好有指向 CPU 制造商规格页面的指针。
  • (要求反汇编的原因:查看编译器是否将自己的向量化应用于您的 C++ 代码。)
  • 其中一些内在函数不对应于单个指令。 AFAIK,madd 实现为add_ps (mul_ps(a,b),c),除非使用 FMA 内在函数。因此,就指令数量而言,这甚至可能算不上什么胜利。并且有很多依赖。标量代码通常会受益于发出并行浮点指令。也就是说,有更好的 SSE 实现here
  • @Brett Hale 没错,你看到的一些“指令”实际上是我写的用来模仿子弹代码中使用的宏的宏。无论哪种方式,它都与 Bullet 使用的相同。

标签: c++ optimization sse quaternions


【解决方案1】:

使用 SIMD 的最佳方法是一次将四(八)个独立四元数与 SSE (AVX) 相乘。然而,这通常是不切实际的。在这种情况下,我建议查看 Agner Fog 的 vectorclass。在special 目录中,他有一个文件quaterinon.h。我转换了乘法函数以匹配您的代码。这只需要 SSE2 (#include &lt;emmintrin.h&gt;)。

inline static __m128 multiplynew(__m128 a, __m128 b) {  
    __m128 a1123 = _mm_shuffle_ps(a,a,0xE5);
    __m128 a2231 = _mm_shuffle_ps(a,a,0x7A);
    __m128 b1000 = _mm_shuffle_ps(b,b,0x01);
    __m128 b2312 = _mm_shuffle_ps(b,b,0x9E);
    __m128 t1    = _mm_mul_ps(a1123, b1000);
    __m128 t2    = _mm_mul_ps(a2231, b2312);
    __m128 t12   = _mm_add_ps(t1, t2);
    const __m128i mask =_mm_set_epi32(0,0,0,0x80000000);
    __m128 t12m  = _mm_xor_ps(t12, _mm_castsi128_ps(mask)); // flip sign bits
    __m128 a3312 = _mm_shuffle_ps(a,a,0x9F);
    __m128 b3231 = _mm_shuffle_ps(b,b,0x7B);
    __m128 a0000 = _mm_shuffle_ps(a,a,0x00);
    __m128 t3    = _mm_mul_ps(a3312, b3231);
    __m128 t0    = _mm_mul_ps(a0000, b);
    __m128 t03   = _mm_sub_ps(t0, t3);
    return         _mm_add_ps(t03, t12m);
}

这是汇编输出(GCC 4.8 MASM 风格):

multiplynew(float __vector, float __vector):
movaps  xmm2, xmm0
movaps  xmm3, xmm0
movaps  xmm5, xmm1
movaps  xmm4, xmm1
shufps  xmm2, xmm0, 229
shufps  xmm4, xmm1, 158
shufps  xmm3, xmm0, 122
shufps  xmm5, xmm1, 1
mulps   xmm3, xmm4
movaps  xmm4, xmm1
mulps   xmm2, xmm5
shufps  xmm4, xmm1, 123
addps   xmm2, xmm3
movaps  xmm3, xmm0
shufps  xmm3, xmm0, 159
shufps  xmm0, xmm0, 0
xorps   xmm2, XMMWORD PTR .LC0[rip]
mulps   xmm3, xmm4
mulps   xmm0, xmm1
subps   xmm0, xmm3
addps   xmm0, xmm2
ret

【讨论】:

  • 感谢您的提示,但它产生的结果与我的其他乘法版本不同。至于速度,它比multiplynew 快,但仍然比multiplyold 慢一点。我查看了反汇编,发现它产生的指令比你发布的要多得多。可能是什么原因造成的?
  • @HaydnV.Harach,你在什么模式下编译?发布模式还是调试模式? 32 位还是 64 位?
  • 发布模式。在 32 位模式下,我的旧参考实现仍然更快,但差距很小。在 64 位模式下,您的版本无疑是最快的,但仍会产生不正确的结果。我的(子弹的)sse 版本比 x64 中的标量版本快。
  • 我认为不正确结果的原因(或至少部分)是 Agner 的代码假定四元数在“wxyz”中,而我的在“xyzw”中。
  • @HaydnV.Harach,有趣的是,64 位的差异如此之大。 64 位有 16 个 SSE 寄存器,而 32 位只有 8 个,所以这可能是原因。你有什么处理器类型?我不认为 Agner 的代码对于这项任务是最优化的(无论如何它只使用 SSE2)。如果您愿意,它可以使用和改进。四元数是 re + im0*i + im1*j + im2*k。所以他假设寄存器是(re,im0,im1,im2)所以我认为你是对的,它是“wxyz”格式。
【解决方案2】:

如果我不得不做出一个疯狂的猜测,我会说这是因为multiply 在指令级是高度可并行化的,而multiplynew 本质上是非常串行的,这是抑制性的。

【讨论】:

    【解决方案3】:

    IMO,使用 SIMD 的最简单和最可扩展的方式是 AoSoA 风格 https://en.wikipedia.org/wiki/AoS_and_SoA

    例如,代替Quaternion { __m128 xyzw; },您可以使用Quaternion { T x, y, z, w; },然后您可以在大部分情况下使用常规四元数,然后在您的大循环中使用Quaternion&lt;__m128&gt; 以四个为一组进行相乘

    大多数其他数学类型也是如此,尤其是向量。我不会担心 IMO 大循环之外的 SIMD,这没关系,这就是为什么我不喜欢 AoS SIMD 样式(随着向量内在函数变大,它无论如何都无法扩展)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-15
      • 1970-01-01
      • 2010-09-29
      • 1970-01-01
      • 1970-01-01
      • 2016-11-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多