读过this interesting article on the results of intrinsic-guided optimization of SSE code in different C++ compilers后,我决定对自己进行测试,特别是因为这篇文章已有几年了 . 我使用的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;
}
}
接下来,我在调试器中编译并运行它,查看反汇编:
Debug:
__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(?),最后到了应该存储它的变量的位置 . 为什么这么多工作?
Release: 这次我希望编译器完全避免将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,xmmword ptr ds:[3E2130h] 003E1010 push esi 003E1011 movaps xmmword ptr [esp 10h],xmm0
显然,不需要ADDPS . 我猜测编译器注意到两个xmmwords是编译时常量,所以它只是添加它们,将结果作为文字放在代码中?奇怪的推动可能与后面的for循环有关,因为就我所知,esi被用作循环计数器 . 仍然,为什么不将数据段中预先计算的文字放入xmm0然后放入局部变量(特别是10h),为什么不直接使用文字?
总而言之,Debug版本比我预期的更愚蠢(或者我可能没有得到什么),而Release版本出乎意料地聪明 . 任何解释此行为的评论将不胜感激 . 谢谢 .
EDIT: 答案非常有启发性,但我仍然想知道是否有任何 I 可以做些什么来改善编译器输出,这就是为什么我要求将此问题解释为当前形式的问题 .
例如,是否有可能以某种方式引导编译器不将foo和bar存储在内存中(因为我在添加后不需要它们),只需将它们加载到xmmN寄存器并保存在那里?可能也会退回?引用文章的作者称MSVC只是"doing exactly what it was told to" . 有没有明确写出__asm块的任何方法可以更好(读取:避免内存传输)代码?谢谢 .
3 回答
这只是代码生成器工作方式的正常副作用 . _mm_set_ps()有 two 个不同的工作要做 . 它首先必须从4个参数中 Build __m128值 . 你选择了简单的方法,它变得更加复杂:
具有完全不同的codegen:
然后第二个工作是将它移动到__m128变量中,这很容易
这还没有优化,只是因为在Debug版本中关闭了优化器 . 代码生成器不会进行任何优化尝试,这不是它的工作 .
当然,优化器能够在编译时计算结果 . 这甚至适用于复杂的例子,你已经看到了这个:
这真是一个关于MSVC内部的问题 . 要得到一个明确的答案,你必须问微软 .
有人可能会推测Release构建的原因在于本地化变量是你已经取得了它的地址 . 获取变量的地址意味着编译器突然不得不处理内存而不是寄存器 . 内存对于编译器来说要困难得多,因为程序中的其他位置可能指向优化器必须考虑的指针 .
您对发布版本的编译时优化是正确的(在目标文件中查找
ds:[3E2130h]
,您将在那里找到添加的值) .是的,调试版似乎做了不必要的工作,但只有2倍,而不是4倍 . 实际上可以期待
存在,但它没有,
MOVAPS
有两种变体,并且都不允许从内存移动到内存(这是x86中的常见情况):调试程序集执行的操作是从目标文件的
.data
部分(最有可能是readonly)读取ds:[0B87840h]
中的xmmword,并将其放在[ebp-190h]
以及foo
的堆栈中 .为了比较,gcc 4.7展示了类似的模式:
我认为这与内置内在函数的实现方式有关 . 例如,
_mm_add_ps
与__m128
参数一起使用,这些参数可能在寄存器中,堆栈上或调用它时的其他位置 . 因此,如果您正在为gcc / VC编写内在函数代码,则必须首先生成将加载值的代码 . 当优化器运行时,它会立即注意到存在不必要的数据推送(但优化器不会在调试版本中运行) .