使用XMM0寄存器和内存提取(C++代码)的速度是仅使用XMM寄存器的ASM的两倍 - 为什么?

eps*_*1on 6 c++ optimization performance assembly sse2

我正在尝试实现一些内联汇编程序(在Visual Studio 2012 C++代码中)以利用SSE.我想添加7个数字1e9次,所以我把它们从RAM放到xmm0到xmm6的CPU寄存器中.当我使用此代码在visual studio 2012中使用内联汇编时:

C++代码:

for(int i=0;i<count;i++)
        resVal+=val1+val2+val3+val4+val5+val6+val7;
Run Code Online (Sandbox Code Playgroud)

我的ASM代码:

int count=1000000000;

    double resVal=0.0;
       //placing values to register
    __asm{  
        movsd xmm0,val1;placing var1 in xmm0 register  
        movsd xmm1,val2  
        movsd xmm2,val3  
        movsd xmm3,val4  
        movsd xmm4,val5  
        movsd xmm5,val6  
        movsd xmm6,val7  
        pxor xmm7,xmm7;//turns xmm7 to zero
         }

    for(int i=0;i<count;i++)
    {
        __asm
        {
            addsd xmm7,xmm0;//+=var1
            addsd xmm7,xmm1;//+=var2
            addsd xmm7,xmm2;
            addsd xmm7,xmm3;
            addsd xmm7,xmm4;
            addsd xmm7,xmm5;
            addsd xmm7,xmm6;//+=var7
        }

    }

    __asm
        {
            movsd resVal,xmm7;//placing xmm7 into resVal
        }
Run Code Online (Sandbox Code Playgroud)

这是来自C++编译器的代码'resVal + = val1 + val2 + val3 + val4 + val5 + val6 + val7'的汇编代码:

movsd       xmm0,mmword ptr [val1]  
addsd       xmm0,mmword ptr [val2]  
addsd       xmm0,mmword ptr [val3]  
addsd       xmm0,mmword ptr [val4]  
addsd       xmm0,mmword ptr [val5]  
addsd       xmm0,mmword ptr [val6]  
addsd       xmm0,mmword ptr [val7]  
addsd       xmm0,mmword ptr [resVal]  
movsd       mmword ptr [resVal],xmm0  
Run Code Online (Sandbox Code Playgroud)

可见,编译器只使用一个xmm0寄存器,有时它从RAM中获取值.

两个代码(我的ASM代码和c ++代码)的答案是相同的,但c ++代码大约需要执行我的asm代码的一半时间!

我对CPU寄存器感到厌烦,使用它们比内存快得多.我不认为这个比例是真的.为什么asm版本的C++代码性能较低?

Mat*_*son 10

  • 一旦数据在缓存中(在第一个循环之后将是这种情况,如果它已经存在),那么如果使用内存或寄存器则没有什么区别.
  • 浮点加法首先需要比单周期更长的时间.
  • 最终存储resVal"解开"xmm0寄存器以允许寄存器自由"重命名",这允许更多的循环并行运行.

这是一个典型的例子,"除非你绝对确定,否则将代码写入编译器".

上面的最后一个解释了为什么代码比代码更快的原因,其中循环的每一步都取决于先前计算的结果.

在编译器生成的代码中,循环可以相当于:

movsd       xmm0,mmword ptr [val1]  
addsd       xmm0,mmword ptr [val2]  
addsd       xmm0,mmword ptr [val3]  
addsd       xmm0,mmword ptr [val4]  
addsd       xmm0,mmword ptr [val5]  
addsd       xmm0,mmword ptr [val6]  
addsd       xmm0,mmword ptr [val7]  
addsd       xmm0,mmword ptr [resVal]  
movsd       mmword ptr [resVal],xmm0  

movsd       xmm1,mmword ptr [val1]  
addsd       xmm1,mmword ptr [val2]  
addsd       xmm1,mmword ptr [val3]  
addsd       xmm1,mmword ptr [val4]  
addsd       xmm1,mmword ptr [val5]  
addsd       xmm1,mmword ptr [val6]  
addsd       xmm1,mmword ptr [val7]  
addsd       xmm1,mmword ptr [resVal]  
movsd       mmword ptr [resVal],xmm1
Run Code Online (Sandbox Code Playgroud)

现在,正如您所看到的,我们可以"混合"这两个"线程":

movsd       xmm0,mmword ptr [val1]  
movsd       xmm1,mmword ptr [val1]  
addsd       xmm0,mmword ptr [val2]  
addsd       xmm1,mmword ptr [val2]  
addsd       xmm0,mmword ptr [val3]  
addsd       xmm1,mmword ptr [val3]  
addsd       xmm0,mmword ptr [val4]  
addsd       xmm1,mmword ptr [val4]  
addsd       xmm0,mmword ptr [val5]  
addsd       xmm1,mmword ptr [val5]  
addsd       xmm0,mmword ptr [val6]  
addsd       xmm1,mmword ptr [val6]  
addsd       xmm0,mmword ptr [val7]  
addsd       xmm1,mmword ptr [val7]  
addsd       xmm0,mmword ptr [resVal]  
movsd       mmword ptr [resVal],xmm0  
// Here we have to wait for resval to be uppdated!
addsd       xmm1,mmword ptr [resVal]  
movsd       mmword ptr [resVal],xmm1
Run Code Online (Sandbox Code Playgroud)

我并不是说它有很多乱序执行,但我当然可以看到循环如何比你的循环更快地执行.如果你有一个备用寄存器,你可以在汇编程序代码中实现相同的功能[在x86_64中你有另外8个寄存器,尽管你不能在x86_64中使用内联汇编程序...]

(注意,寄存器重命名不同于我的"线程"循环,它使用两个不同的寄存器 - 但效果大致相同,循环可以在它到达"resVal"更新后继续而不必等待结果更新)

  • @ epsi1on汇编代码在`xmm7`上有一个数据依赖链.编译器生成的代码不会,因为`xmm0`的所有商店都是死存储. (2认同)
  • 寄存器重命名意味着在上一次迭代完成更新`resVal`之前,通过`val7`添加`val1`可以沿着管道继续(使用不同的物理寄存器). (2认同)