如何改善编译器对我的SSE指令的处理?

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,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中,另一个存储在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);
在这段代码中,使用了SSE指令集的_mm_set_ps和_mm_add_ps函数进行浮点数运算。编译器可能会将常量编译成字面量,因此不需要使用ADDPS指令。push esi指令可能与后续循环有关,esi用作循环计数器。为什么要将预先计算好的字面量从数据段加载到xmm0寄存器,然后再放到本地变量(esp+10h)中,而不是直接使用字面量呢?Debug版本比预期更愚蠢,Release版本则出乎意料地聪明。如果有任何解释这种行为的评论,将不胜感激。如果想要改善编译器输出,是否有任何方法可以避免内存传输,例如将foo、bar和ret直接加载到xmmN寄存器中并保留在那里,而不是将它们存储在内存中?作者称MSVC只是“按照指令执行”,是否有方法可以获得更好的代码(即避免内存传输),而无需显式编写__asm块?
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]  

谢谢 @Hans,您能否也请处理一下 Q 的编辑?谢谢。 - neuviemeporte
没有必要采取任何措施来改进它。只需构建项目的发布版本即可完成。在调试构建中启用优化器只会使调试变得更加困难,调试构建的目的是使其更容易调试。如果您确实想这样做,可以帮助了解它如何变得更难。 - Hans Passant
好的,但是如果输入不是编译时常量:在SSE代码中有什么需要避免的以便编译器可以自由进行优化?@Søren提到不要对__m128取地址。还有其他的吗? - neuviemeporte

1

关于发布版本的编译时优化,你是正确的(在你的目标文件中查找ds:[3E2130h],你会发现添加的值)。

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

 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 中。与此类似,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++编写固有函数代码,你需要首先生成一个将值加载到内存中的代码。当优化器运行时,它立即注意到存在不必要的数据推送(但优化器不会在调试版本中运行)。

关于“人们会期望存在movaps [mem],[mem]......”- 不,这种指令,即直接内存-内存复制,在x86中除了字符串移动(rep movs)之外的任何地方都不存在。 x86中的所有其他指令(包括所有SSE / AVX指令)只能有一个内存操作数。 - FrankH.
@FrankH。这正是我所说的,如果你继续阅读:“(这是x86中通常的情况)”。然而,如果你对汇编语言还比较新,期望存在“[mem]”,“[mem]”指令并不是不合理的,这是一个常见的困惑来源。 - us2012

1
这其实是关于MSVC内部的问题。要得到明确的答案,你需要向Microsoft提问。
有人可能会猜测,Release版本将ret放入本地变量的原因是你已经取得了它的地址。取一个变量的地址意味着编译器突然必须处理内存而不是寄存器。内存对编译器来说更难处理,因为程序中的其他位置可能有指向该变量的指针,优化器必须考虑到这一点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接