3

在阅读了这篇关于在不同 C++ 编译器中对 SSE 代码进行内在引导优化的结果的有趣文章后,我决定自己做一个测试,特别是因为这篇文章已经有几年了。我使用了 MSVC,它在帖子作者执行的测试中表现非常糟糕(尽管在 VS 2010 版本中)并决定坚持一个非常基本的场景:将一些值打包到 XMM 寄存器中并执行简单的操作,如加法. 在文章中,_mm_set_ps 翻译成一个奇怪的标量移动和解包指令序列,所以让我们看看:

int _tmain(int argc, _TCHAR* argv[])
{
    __m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
    __m128 bar = _mm_set_ps(5.0f, 6.0f, 7.0f, 8.0f);
    __m128 ret = _mm_add_ps(foo, bar);

    // need to do something so vars won't be optimized out in Release
    float *f = (float *)(&ret);
    for (int i = 0; i < 4; i++) 
    {
        cout << "f[" << i << "] = " << f[i] << endl;
    }
}

接下来,我在调试器中编译并运行它,查看反汇编:

调试:

__m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
00B814F0 movaps xmm0,xm​​mword ptr ds:[0B87840h]
00B814F7 movaps xmmword ptr [ebp-190h],xmm0
00B814FE movaps xmm0,xm​​mword ptr [ebp-190h]
00B81505 movaps xmmword ptr [08],xmm0
_psm.2 6.0f、7.0f、8.0f);
00B81509 movaps xmm0,xm​​mword ptr ds:[0B87850h]
00B81510 movaps xmmword ptr [ebp-170h],xmm0
00B81517 movaps xmm0,xm​​mword ptr [ebp-170h]
00B8151E movaps xmmword ptr,bar],_
mm_add_ps1( );
00B81522 movaps xmm0,xm​​mword ptr [bar]
00B81526 movaps xmm1,xmmword ptr [foo]
00B8152A addps xmm1,xmm0
00B8152D movaps xmmword ptr [ebp-150h],xmm1
00B81534 movaps xmm0,xm​​mword ptr [ebp-150h]
00B8153B movaps xmmword ptr [ret],xmm0

完全糊涂;为什么将 xmmword 放入 __m128 需要四个 MOVAPS?首先,它将数据放入 xmm0 (我假设它是存储在某处的四个浮点值的文字,不知道如何查看它),然后将 xmm0 复制到 ebp 指向的某处和一个偏移量,然后再将其复制回来到 xmm0 (?),最后到应该存储它的变量的位置。为什么要做这么多工作?

发布: 这次我希望编译器完全避免将 xmmword 存储在内存中,只需将一个放在 xmm0 中,另一个放在 xmm1 中,执行 ADDPS,将结果放在内存中并完成它。相反,我得到了:

__m128 foo = _mm_set_ps(1.0f, 2.0f, 3.0f, 4.0f);
__m128 bar = _mm_set_ps(5.0f, 6.0f, 7.0f, 8.0f);
__m128 ret = _mm_add_ps(foo, bar);
003E1009 movaps xmm0,xm​​mword ptr ds:[3E2130h]
003E1010 push esi
003E1011 movaps xmmword ptr [esp+10h],xmm0

显然,不需要 ADDPS。我猜编译器注意到这两个 xmmwords 是编译时常量,所以它只是添加了它们,将结果作为文字放入代码中?奇怪的推动可能与随后的 for 循环有关,因为据我所知, esi 在那里用作循环计数器。不过,为什么要将数据段中预先计算的文字放入 xmm0 中,然后放入局部变量(esp+10h),为什么不直接使用文字呢?

总而言之,Debug 版本比我预期的更愚蠢(或者我可能没有得到什么),而 Release 版本却出人意料地聪明。任何解释此行为的评论将不胜感激。谢谢。

编辑:答案非常有启发性,但我仍然想知道是否可以做些什么来改进编译器输出,这就是为什么我将问题从要求对此进行解释更改为当前形式。

例如,是否有可能以某种方式引导编译器不将foobar存储在内存中(因为添加后我不需要它们),只需将它们加载到 xmmN 寄存器并将它们保留在那里?也有可能退吗?引用文章的作者说,MSVC 只是“完全按照它的指示行事”。有什么方法可以在不显式编写 __asm 块的情况下变得更好(阅读:避免内存传输)代码?谢谢。

4

3 回答 3

5

这只是代码生成器工作方式的正常副作用。_mm_set_ps() 有两个不同的工作要做。它首先必须从 4 个参数中建立 __m128 值。你选择了简单的方法,它变得更加复杂:

float x = 1.0f;
__m128 foo = _mm_set_ps(x, 2.0f, 3.0f, 4.0f);

使用截然不同的代码生成:

00C513DD  movss       xmm0,dword ptr ds:[0C5585Ch]  
00C513E5  movss       xmm1,dword ptr [x]  
00C513EA  movaps      xmm2,xmmword ptr ds:[0C55860h]  
00C513F1  unpcklps    xmm0,xmm1  
00C513F4  unpcklps    xmm2,xmm0  
00C513F7  movaps      xmmword ptr [ebp-100h],xmm2

第二个工作是把它移到 __m128 变量中,这很容易

00C513FE  movaps      xmm0,xmmword ptr [ebp-100h]  
00C51405  movaps      xmmword ptr [foo],xmm0  

这还没有优化仅仅是因为优化器在 Debug 构建中被关闭。代码生成器不会尝试优化,这不是它的工作。

当然,优化器能够在编译时计算结果。这甚至适用于复杂的示例,您已经看到了:

00EE1284  movaps      xmm0,xmmword ptr ds:[0EE3260h]  
于 2013-02-14T05:16:20.913 回答
1

您对发布版本的编译时优化是正确的(ds:[3E2130h]在您的目标文件中查找,您会在那里找到添加的值)。

是的,调试版本似乎做了不必要的工作,但只有 2 倍,而不是 4 倍。人们实际上会期望

 movaps xmmword ptr [foo],xmmword ptr ds:[0B87840h]

存在,但它不存在,MOVAPS有两种变体,并且都不允许从内存移动到内存(这是 x86 中的常见情况):

MOVAPS xmm1,xmm2/mem128       ; 0F 28 /r        [KATMAI,SSE]
MOVAPS xmm1/mem128,xmm2       ; 0F 29 /r        [KATMAI,SSE]

ds:[0B87840h]调试程序集所做的是从目标文件的.data部分(很可能是只读的)中读取 xmmword ,并将其放入堆栈[ebp-190h]中的foo.

作为比较,gcc 4.7 展示了类似的模式:

movaps  xmm0, XMMWORD PTR .LC0[rip] # D.5374,
movaps  XMMWORD PTR [rbp-64], xmm0  # foo, D.5353
movaps  xmm0, XMMWORD PTR .LC1[rip] # D.5381,
movaps  XMMWORD PTR [rbp-48], xmm0  # bar, D.5354
movaps  xmm0, XMMWORD PTR [rbp-64]  # tmp79, foo
movaps  XMMWORD PTR [rbp-32], xmm0  # __A, tmp79
movaps  xmm0, XMMWORD PTR [rbp-48]  # tmp80, bar
movaps  XMMWORD PTR [rbp-16], xmm0  # __B, tmp80
movaps  xmm0, XMMWORD PTR [rbp-16]  # tmp81, __B
movaps  xmm1, XMMWORD PTR [rbp-32]  # tmp82, __A
addps   xmm0, xmm1  # D.5386, tmp82

我认为这与内置内在函数的实现方式有关。例如,_mm_add_ps使用在__m128调用时可能位于寄存器、堆栈或其他地方的参数。因此,如果您正在为 gcc/VC++ 编写内部代码,则必须首先生成将加载这些值的代码。当优化器运行时,它会立即注意到有不必要的数据推送(但优化器不会在调试版本中运行)。

于 2013-02-14T04:41:39.887 回答
1

这确实是一个关于 MSVC 内部的问题。要获得明确的答案,您必须询问 Microsoft。

有人可能会推测 Release 构建将 ret 放入局部变量的原因是您获取了它的地址。获取变量的地址意味着编译器突然不得不处理内存而不是寄存器。内存对于编译器来说要困难得多,因为程序中的其他地方可能有指向优化器必须考虑的指针。

于 2013-02-14T04:52:21.330 回答