为什么编译器内联产生比手动内联更慢的代码?

Joh*_*rer 31 c++ performance assembly inlining compiler-optimization

背景

以C++编写的一个数值软件的以下关键循环基本上将两个对象的成员比较:

for(int j=n;--j>0;)
    asd[j%16]=a.e<b.e;
Run Code Online (Sandbox Code Playgroud)

ab是类ASD:

struct ASD  {
    float e;
    ...
};
Run Code Online (Sandbox Code Playgroud)

我正在调查将此比较放在轻量级成员函数中的效果:

bool test(const ASD& y)const {
    return e<y.e;
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

for(int j=n;--j>0;)
    asd[j%16]=a.test(b);
Run Code Online (Sandbox Code Playgroud)

编译器正在内联这个函数,但问题是,汇编代码会有所不同,导致运行时开销超过10%.我要问:

问题

  1. 为什么编译器会推出不同的汇编代码?

  2. 为什么生产的组件更慢?

编辑: 第二个问题已通过实施@ KamyarSouri的建议(j%16)得到了回答.汇编代码现在看起来几乎相同(请参阅http://pastebin.com/diff.php?i=yqXedtPm).唯一的区别是第18,33,48行:

000646F9  movzx       edx,dl 
Run Code Online (Sandbox Code Playgroud)

材料

此图表显示了我的代码的50个测试的FLOP/s(最多为缩放因子).

在此输入图像描述

用于生成绘图的gnuplot脚本:http://pastebin.com/8amNqya7

编译器选项:

/ Zi/W3/WX-/MP/Ox/Ob2/Oi/Ot/Oy/GL/D"WIN32"/ D"NDEBUG"/ D"_CONSOLE"/ D"_UNICODE"/ D"UNICODE"/ Gm-/EHsc/MT/GS-/Gy/arch:SSE2/fp:exact/Zc:wchar_t/Zc:forScope/Gd/analyze-

链接器选项:/ INCREMENTAL:NO"kernel32.lib""user32.lib""gdi32.lib""winspool.lib""comdlg32.lib""advapi32.lib""shell32.lib""ole32.lib""oleaut32. lib""uuid.lib""odbc32.lib""odbccp32.lib"/ ALLOWISOLATION/MANIFESTUAC:"level ='asInvoker'uiAccess ='false'"/ SUBSYSTEM:CONSOLE/OPT:REF/OPT:ICF/LTCG/TLBID :1/DYNAMICBASE/NXCOMPAT/MACHINE:X86/ERRORREPORT:QUEUE

Mys*_*ial 31

简答:

您的asd数组声明为:

int *asd=new int[16];
Run Code Online (Sandbox Code Playgroud)

因此,使用int返回类型而不是bool.
替代,将数组类型更改为bool.

在任何情况下,使test函数的返回类型与数组的类型匹配.

跳到底部了解更多详情.

答案很长:

在手动内联版本中,一次迭代的"核心"如下所示:

xor         eax,eax  

mov         edx,ecx  
and         edx,0Fh  
mov         dword ptr [ebp+edx*4],eax  
mov         eax,dword ptr [esp+1Ch]  
movss       xmm0,dword ptr [eax]  
movss       xmm1,dword ptr [edi]  
cvtps2pd    xmm0,xmm0  
cvtps2pd    xmm1,xmm1  
comisd      xmm1,xmm0  
Run Code Online (Sandbox Code Playgroud)

除第一条指令外,编译器内联版本完全相同.

而不是:

xor         eax,eax
Run Code Online (Sandbox Code Playgroud)

它有:

xor         eax,eax  
movzx       edx,al
Run Code Online (Sandbox Code Playgroud)

好的,这是一条额外的指令.他们都做同样的事情 - 归零寄存器.这是我看到的唯一区别......

movzx指令0.33在所有较新的架构上具有单周期延迟和周期倒数吞吐量.所以我无法想象这会如何产生10%的差异.

在这两种情况下,归零的结果仅在以后使用3条指令.因此,很可能这可能是关键的执行路径.


虽然我不是英特尔工程师,但我的猜测是:

大多数现代处理器xor eax,eax通过寄存器重命名到零寄存器组来处理归零操作(例如).它完全绕过执行单元.但是,当通过访问(部分)寄存器时,这种特殊处理可能会导致管道冒泡movzx edi,al.

此外,在编译器内联版本中也存在错误依赖eax:

movzx       edx,al  
mov         eax,ecx  //  False dependency on "eax".
Run Code Online (Sandbox Code Playgroud)

无论是否乱序执行能够解决,这是超越我.


好的,这基本上变成了对MSVC编译器进行逆向工程的问题......

在这里,我将解释为什么movzx产生额外的原因以及为什么它会停留.

这里的关键是bool返回值.显然,bool数据类型可能是MSVC内部表示中存储的8位值.因此,当您隐式转换boolint此处时:

asd[j%16] = a.test(b);
^^^^^^^^^   ^^^^^^^^^
 type int   type bool
Run Code Online (Sandbox Code Playgroud)

有一个8位 - > 32位整数提升.这就是MSVC生成movzx指令的原因.

当手动完成内联时,编译器有足够的信息来优化这种转换,并将所有内容保存为32位数据类型IR.

但是,当代码使用bool返回值放入其自己的函数时,编译器无法优化8位中间数据类型.因此,movzx留下来.

当你使双方数据类型相同(或intbool),无需转换.因此完全避免了这个问题.

  • 这些出色的答案真的激励我学习装配 (6认同)