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

31

背景

下面这段用 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.为什么生成的汇编速度较慢?
编辑:通过实现@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 "WIN32" /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm- /EHsc /MT /GS- /Gy /arch:SSE2 /fp:precise /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


10
哦,天啊……有人似乎已经学会了提出精彩问题的艺术……但这一次,我脑袋里不知道答案。 - Mysticial
也许可以尝试其他编译器?我一直没有用 MSVC 编译出高质量的代码。可以试试 Borland 和 gcc。 - wallyk
1
@Johannes Gerer:与 a.e<b.e 相比,j%10 可能需要更长的时间。你可以尝试将 j%10 替换为类似于 j%16 的东西,并用 &15 替换它来重新进行测试吗? - Kamyar Souri
1
嗯,在这种情况下,显然是微软编译器,所以这可能是问题的一个重要因素。但正如其他人暗示的那样,编译器可能至少部分地认为它处于调试模式,或者优化被“降低”了。 - Hot Licks
1
是的,从Mystical的帖子中看来,这似乎主要是“运气不佳”。优化有一定的统计性质--同样的优化99%的时间会让你受益,但1%的时间会让你吃亏。在xor实现上有点粗心通常不会有影响,但也许在这种情况下有影响(或者问题可能与轻微的缓存边界差异或类似问题无关)。我甚至见过程序在多次重新编译时以不同的速度运行的情况,这仅仅基于它如何映射到内存。 - Hot Licks
显示剩余14条评论
2个回答

32

简短回答:

你的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个指令后使用。因此,这很可能位于执行的关键路径上。


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

大多数现代处理器通过寄存器重命名来处理清零操作(例如xor eax,eax),以使用一组零寄存器。它完全绕过了执行单元。然而,当通过movzx edi,al访问(部分)寄存器时,这种特殊处理可能会导致流水线泡沫。

此外,在编译器内联版本中,还存在对eax假依赖

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

无论乱序执行能否解决这个问题,都超出了我的能力范围。


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

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

关键在于bool返回值。显然,在MSVC内部表示中,bool数据类型可能存储为8位值。 因此,当你从bool隐式转换为int时,就会发生以下情况:

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

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

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

然而,当代码放入自己的函数中并具有bool返回值时,编译器无法优化掉8位中间数据类型。因此,movzx保留。

当您使两个数据类型相同时(intbool),不需要转换。因此,问题完全避免了。


实际上,同样的事情适用。将“%10”替换为“%16”仅会消除乘法和移位逻辑。movzx仍然存在 - 只是在编译器内联版本中存在。 - Mysticial
是的,这是另一个有趣的问题需要回答。黑入编译器和汇编... - Mysticial
那些额外的完全冗余的movzx指令改变了代码的其余部分的对齐方式。由于这个循环超过了28个uops,在Nehalem或SnB上无法适应循环缓冲区。(我认为它更像是47个uops,假设具有两个寄存器寻址模式的存储器不能微融合。因此,它可以适应禁用超线程的Haswell的循环缓冲区。)在Nehalem上,这意味着解码器会发挥作用。在Intel SnB及更高版本中,对齐方式会影响uop缓存。OP没有说这是在哪个CPU上测试的,但10%很容易可信。这是一个前端瓶颈。 - Peter Cordes
仅供记录,这个函数的实现非常糟糕。将movzx到一个即将被覆盖而没有被读取的寄存器中真的很愚蠢。使用jbe / mov reg, 1 / jmp / xor reg,reg代替setbe dl也非常愚蠢。如果要展开,应该放弃一些每次迭代的增量/减量,并使用具有不同偏移量的相同地址。哎呀,实际上每次都是相同的标量测试?把它从循环中拿出来...我不知道它为什么要重新从堆栈中加载地址。 - Peter Cordes
由于没有人在这个问题上提供链接,所以这是必读的:http://agner.org/optimize/ - Peter Cordes
显示剩余3条评论

1

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

编辑:

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

尽管如此,编译器仍然似乎不确定这是调试版还是发布版。

另一个编辑:

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


是的,但 lea esp,[esp] 在(更快的)手动内联版本中! - Johannes Gerer
我如何坚持“发布版本”?您可以在原帖中找到我使用的编译器选项。 - Johannes Gerer

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