如何改进编译器对我的SSE内在函数的处理?
在阅读了关于不同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,xmmword ptr ds:[0B87840h]
00B814F7 movaps xmmword ptr [ebp-190h],xmm0
00B814FE movaps xmm0,xmmword ptr [ebp-190h]
00B81505 movaps xmmword ptr [foo],xmm0
__m128 bar = _mm_set_ps(5.0f,6.0f,7.0f,8.0f);
00B81509 movaps xmm0,xmmword ptr ds:[0B87850h]
00B81510 movaps xmmword ptr [ebp-170h],xmm0
00B81517 movaps xmm0,xmmword ptr [ebp-170h]
00B8151E movaps xmmword ptr [bar],xmm0
__m128 ret = _mm_add_ps(foo,bar);
00B81522 movaps xmm0,xmmword ptr [bar]
00B81526 movaps xmm1,xmmword ptr [foo]
00B8152A addps xmm1,xmm0
00B8152D movaps xmmword ptr [ebp-150h],xmm1
00B81534 movaps xmm0,xmmword ptr [ebp-150h]
00B8153B movaps xmmword ptr [ret],xmm0
完全困惑; 为什么将xmmword放入__m128需要四个MOVAPS? 首先,它将数据放入xmm0中(我认为这是四个浮点值存储在某处的文字,不知道如何查看它),然后将xmm0复制到某处由ebp指向的位置和一个偏移量,仅将其复制回来到xmm0(?),最后到应该存储变量的位置。 为什么这么多工作?
发布:这次我期待编译器避免将xmmword存储在内存中,只需将xmm0放在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,xmmword ptr ds:[3E2130h]
003E1010推esi
003E1011 movaps xmmword ptr [esp + 10h],xmm0
显然,不需要ADDPS。 我猜测编译器注意到这两个xmmwords是编译时常量,所以它只是添加了它们,将结果作为文字输入到代码中? 奇怪的推动可能与随后的for循环有关,因为esi被用作循环计数器,据我所知。 尽管如此,为什么不将数据段中的预先计算的文字放入xmm0,然后放入局部变量(esp + 10h),为什么不直接使用文字?
总而言之,Debug版本比我预期的要更愚蠢(或者我可能没有收到什么东西),而发布版本却意想不到。 任何意见解释这种行为将不胜感激。 谢谢。
编辑:答案是非常有启发性的,但我仍然想知道是否有任何我可以做的改进编译器输出,这就是为什么我改变问题从要求解释这到当前的形式。
例如,是否有可能以某种方式指导编译器不将foo和bar存储在内存中(因为在添加之后我不需要它们),只需将它们加载到xmmN寄存器并将它们保存在那里? 可能也回收? 引用文章的作者说,MSVC只是“按照它所告诉的那样做”。 任何方式来改善(读:避免内存传输)代码,而不明确写一个__asm块? 谢谢。
这只是代码生成器工作方式的常见副作用。 _mm_set_ps()有两个不同的工作要做。 它首先必须建立4个参数中的__m128值。 你选择了简单的方式,它变得更加复杂:
float x = 1.0f;
__m128 foo = _mm_set_ps(x, 2.0f, 3.0f, 4.0f);
随着codegen的截然不同:
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]
这实际上是一个关于MSVC内部的问题。 要得到明确答案,你必须问问微软。
有人可能会推测,Release版本将ret放入本地变量的原因是您已经采用了它的地址。 考虑变量的地址意味着编译器突然不得不处理内存而不是寄存器。 内存对于编译器来说非常困难,因为程序中的其他地方可能有指向优化器必须考虑的指针。
您对版本构建的编译时优化是正确的(在对象文件中查找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]
调试程序集所做的是从目标文件的.data
节中的ds:[0B87840h]
中读取xmmword(很可能是只读的),并将它放在[ebp-190h]
以及foo
的堆栈中。
为了比较,海湾合作委员会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
参数一起使用, __m128
参数可能在寄存器,堆栈中或调用时的其他地方。 因此,如果您正在为gcc / VC ++编写内部代码,则必须先生成将加载值的代码。 优化程序运行时,会立即发现无需推送数据(但优化程序不会在调试版本中运行)。
上一篇: How can I improve the compiler's handling of my SSE intrinsics?
下一篇: How to Make a Call to Koa.js App Instance for Unit Tests