背景
以下用 C++ 编写的数值软件的关键循环主要通过其中一个成员比较两个对象:
for(int j=n;--j>0;)
asd[j%16]=a.e<b.e;
a
和 b
属于 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% 的运行时开销。我不得不质疑:
问题
为什么编译器会产生不同的汇编代码?
为什么生成的程序集比较慢?
编辑:通过实现@KamyarSouri 的建议 (j%16) 已经回答了第二个问题。汇编代码现在看起来几乎相同(参见 http://pastebin.com/diff.php?i=yqXedtPm)。唯一的区别是第 18、33、48 行:
000646F9 movzx edx,dl
Material
- 测试代码:http://pastebin.com/03s3Kvry
- MSVC10 上带有/Ox/Ob2/Ot/arch:SSE2 的程序集输出:
此图表显示了我的代码 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
最佳答案
简答:
您的 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
访问(部分)寄存器时,这种特殊处理可能会导致管道气泡。
此外,编译器内联版本中还有一个false对eax
的依赖:
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
保留。
当您使两种数据类型相同(int
或 bool
)时,不需要转换。因此完全避免了这个问题。
关于c++ - 为什么编译器内联产生的代码比手动内联慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8583964/