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

时间:2011-12-21 01:04:53

标签: c++ performance assembly compiler-optimization inlining

背景

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

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

ab属于ASD类:

struct ASD  {
    float e;
    ...
};

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

bool test(const ASD& y)const {
    return e<y.e;
}

并像这样使用它:

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

编译器正在内联此函数,但问题是,汇编代码将不同并导致> 10%的运行时开销。我不得不质疑:

问题

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

  2. 为什么生产的程序集会变慢?

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

    000646F9  movzx       edx,dl 
    

    材料

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

    enter image description here

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

    编译器选项:

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

    链接器选项: / INCREMENTAL:NO&#34; kernel32.lib&#34; &#34; USER32.LIB&#34; &#34; GDI32.LIB&#34; &#34; winspool.lib&#34; &#34; comdlg32.lib&#34; &#34; advapi32.lib&#34; &#34; SHELL32.LIB&#34; &#34; ole32.lib&#34; &#34; oleaut32.lib&#34, &#34; UUID.LIB&#34; &#34; odbc32.lib&#34; &#34; odbccp32.lib&#34; / ALLOWISOLATION / MANIFESTUAC:&#34; level =&#39; asInvoker&#39; uiAccess =&#39;假&#39;&#34; / SUBSYSTEM:CONSOLE / OPT:REF / OPT:ICF / LTCG / TLBID:1 / DYNAMICBASE / NXCOMPAT / MACHINE:X86 / ERRORREPORT:QUEUE

2 个答案:

答案 0 :(得分:31)

简答:

您的asd数组声明为:

int *asd=new int[16];

因此,请使用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  

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

而不是:

xor         eax,eax

它有:

xor         eax,eax  
movzx       edx,al

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

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

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


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

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

此外,编译器内联版本中eax还存在 false 依赖关系:

movzx       edx,al  
mov         eax,ecx  //  False dependency on "eax".

out-of-order execution能否解决这个问题超出了我的范围。


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

在这里,我将解释 为什么 生成额外的movzx以及它保留的原因。

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

asd[j%16] = a.test(b);
^^^^^^^^^   ^^^^^^^^^
 type int   type bool

有一个8位 - &gt; 32位整数提升。这就是MSVC生成movzx指令的原因。

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

但是,当代码被置于具有bool返回值的自己的函数中时,编译器无法优化8位中间数据类型。因此,movzx会停留。

如果两种数据类型相同(intbool),则不需要转换。因此,完全避免了这个问题。

答案 1 :(得分:1)

lea esp,[esp]占用7个字节的i-cache并且它在循环内部。其他一些线索使得看起来编译器不确定这是发布版本还是调试版本。

编辑:

lea esp,[esp]不在循环中。周围指令中的位置误导了我。现在它看起来有意浪费7个字节,然后是另外浪费的2个字节,以便在16字节边界处启动实际循环。这意味着这实际上加快了速度,正如Johennes Gerer所观察到的那样。

编译器似乎仍不确定这是否是调试或发布版本。

另一个编辑:

pastebin diff与我之前看到的pastebin diff不同。这个答案现在可以删除,但它已经有了评论,所以我会留下它。