31

背景

下面是一个用 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% 的运行时开销。我不得不质疑:

问题

  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-

链接器选项:/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

4

2 回答 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返回值。显然,bool数据类型可能是存储在 MSVC 内部表示中的 8 位值。因此,当您从此处隐式转换boolint

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

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

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

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

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

于 2011-12-21T01:54:31.370 回答
1

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

编辑:

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

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

另一个编辑:

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

于 2011-12-21T01:36:14.390 回答