首页 文章

如何改进编译器对SSE内在函数的处理?

提问于
浏览
3

读过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 回答

  • 1

    这只是代码生成器工作方式的正常副作用 . _mm_set_ps()有 two 个不同的工作要做 . 它首先必须从4个参数中 Build __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]
    
  • 5

    这真是一个关于MSVC内部的问题 . 要得到一个明确的答案,你必须问微软 .

    有人可能会推测Release构建的原因在于本地化变量是你已经取得了它的地址 . 获取变量的地址意味着编译器突然不得不处理内存而不是寄存器 . 内存对于编译器来说要困难得多,因为程序中的其他位置可能指向优化器必须考虑的指针 .

  • 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]
    

    调试程序集执行的操作是从目标文件的 .data 部分(最有可能是readonly)读取 ds:[0B87840h] 中的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编写内在函数代码,则必须首先生成将加载值的代码 . 当优化器运行时,它会立即注意到存在不必要的数据推送(但优化器不会在调试版本中运行) .

相关问题