【问题标题】:Why does compiler inlining produce slower code than manual inlining?为什么编译器内联产生的代码比手动内联慢?
【发布时间】:2012-01-24 22:03:44
【问题描述】:

背景

以下用 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(最大比例因子)。

生成绘图的 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-

链接器选项: /增量:没有“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

【问题讨论】:

  • 好问题。发布用于产生此结果的优化和其他相关编译器设置可能具有指导意义。
  • 哦,伙计……似乎有人学会了提出一流问题的艺术……只是这一次,我脑子里没有答案……
  • @Johannes Gerer:与a.e&lt;b.e 相比,j%10 可能需要相当长的时间。您可以尝试通过将j%10 替换为j%16 之类的东西来重新进行测试吗?
  • 好吧,在这种情况下,它显然是一个 M$ 编译器,所以这可能是一大块问题。但正如其他人所暗示的那样,编译器可能至少部分认为它处于调试模式,或者优化“已关闭”。
  • 是的,从 Mystical 的帖子来看,这似乎主要是“运气不好”。优化有一定的统计性质——同样的优化在 99% 的时间里“获胜”,但在 1% 的时间里会咬你一口。并且对 xor 实现有点粗心通常无关紧要,但在这种情况下可能会发生(或者问题可能是不相关的,并且由于缓存边界的轻微差异等原因。我什至见过程序运行的情况多次重新编译时以不同的速度,这取决于它如何映射到内存。

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


【解决方案1】:

简答:

您的 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 访问(部分)寄存器时,这种特殊处理可能会导致流水线气泡。

此外,在编译器内联版本中还有一个对eaxfalse 依赖:

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

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


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

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

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

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

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

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

但是,当代码被放入它自己的函数中并返回值 bool 时,编译器无法优化出 8 位中间数据类型。因此,movzx 保持不变。

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

【讨论】:

  • 对不起,我刚刚在更新程序后发现了相同的指令。好吧,正如您知道提出出色答案的艺术一样,这应该会损害您的答案。
  • 其实也一样。用%16 替换%10 只会摆脱乘法和移位逻辑。 movzx 仍然存在 - 仅在编译器内联版本中。
  • 想一想,我认为您的编辑加强了我的回答...它消除了%10 的噪音并增加了一个新的停顿原因。
  • 你解决了!您可以将其称为更高形式的返回值优化(此处缺少 MSVC)。在我发布这个奇怪的代码之前,我能想到的唯一原因实际上是bool? -&gt; int`的转换,但我立即放弃了这个想法。
  • 是的,这是另一个有趣的问题。侵入编译器和程序集...
【解决方案2】:

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

编辑:

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

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

另一个编辑:

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

【讨论】:

  • 是的,但是lea esp,[esp] 是(更快的)手动内联版本!
  • 我怎么能坚持“发布版本”?您可以找到我在 OP 中使用的编译器选项
猜你喜欢
  • 2018-06-16
  • 1970-01-01
  • 1970-01-01
  • 2019-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-15
  • 1970-01-01
相关资源
最近更新 更多