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

标签 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. 为什么生成的程序集比较慢?

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

000646F9  movzx       edx,dl 

Material

此图表显示了我的代码 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-

链接器选项: /增量:没有“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 访问(部分)寄存器时,这种特殊处理可能会导致管道气泡。

此外,编译器内联版本中还有一个falseeax的依赖:

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)时,不需要转换。因此完全避免了这个问题。

关于c++ - 为什么编译器内联产生的代码比手动内联慢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8583964/

相关文章:

sql - 在索引/唯一字段上查询时使用 MySQL "LIMIT 1"有什么意义吗?

c++ - 使用 C 中系统调用的文件描述符

c# - 如何使用CreateExternalTexture和从C++库传递的指针创建Texture2D?

c++ - Overloaded operator new 在内部是如何工作的?

c++ - C\C++ 中的 Windows USB 设备刷新

c++ - 使用 std::sort 在自定义类 C++ 中对 vector 进行排序

android - 快速视频流和上传 Android

.net - linq 表达式的构造方式是否存在性能差异?

c - 设置静态分配对象的内存位置

assembly - ARM指令含义