Eli*_*sky 15 c++ optimization g++ visual-c++
我正在使用不同的编译器遇到以下代码的不一致优化行为:
class tester
{
public:
tester(int* arr_, int sz_)
: arr(arr_), sz(sz_)
{}
int doadd()
{
sm = 0;
for (int n = 0; n < 1000; ++n)
{
for (int i = 0; i < sz; ++i)
{
sm += arr[i];
}
}
return sm;
}
protected:
int* arr;
int sz;
int sm;
};
Run Code Online (Sandbox Code Playgroud)
该doadd
函数模拟对成员的一些密集访问(忽略此问题的溢出).与作为函数实现的类似代码相比:
int arradd(int* arr, int sz)
{
int sm = 0;
for (int n = 0; n < 1000; ++n)
{
for (int i = 0; i < sz; ++i)
{
sm += arr[i];
}
}
return sm;
}
Run Code Online (Sandbox Code Playgroud)
在使用Visual C++ 2008在发布模式下编译时,该doadd
方法的运行速度比arradd
函数慢1.5倍.当我将doadd
方法修改为如下(使用locals别名所有成员)时:
int doadd()
{
int mysm = 0;
int* myarr = arr;
int mysz = sz;
for (int n = 0; n < 1000; ++n)
{
for (int i = 0; i < mysz; ++i)
{
mysm += myarr[i];
}
}
sm = mysm;
return sm;
}
Run Code Online (Sandbox Code Playgroud)
运行时变得大致相同.我是否正确地认为这是Visual C++编译器缺少的优化?g++
似乎做得更好,并在使用-O2
或编译时以相同的速度运行成员函数和正常函数-O3
.
基准测试是通过在一些足够大的数组(大小为几百万个整数)上调用doadd
成员和arradd
函数来完成的.
编辑:一些细粒度的测试表明,罪魁祸首是sm
成员.用本地版本替换所有其他版本仍然会使运行时变长,但是一旦我sm
被mysm
运行时替换就变得等于函数版本.
对答案感到失望(抱歉的家伙),我摆脱了懒惰,潜入了这段代码的反汇编列表中.我在下面的回答总结了调查结果.简而言之:它与别名无关,它与循环展开有关,并且在决定展开哪个循环时会应用一些奇怪的启发式MSVC.
它可能是一个别名问题 - 编译器无法知道实例变量sm
永远不会被指向arr
,因此它必须将其sm
视为有效的volatile,并将其保存在每次迭代中.您可以sm
使用不同的类型来测试此假设.或者只使用一个临时的本地总和(它将被缓存在一个寄存器中)并sm
在最后分配它.
我用 MSVC 反汇编了代码,以便更好地理解发生了什么。事实证明,别名根本不是问题,而且某种偏执的线程安全也不是问题。
这是反汇编函数中有趣的部分arradd
:
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
013C101C mov ecx,ebp
013C101E mov ebx,29B9270h
{
sm += arr[i];
013C1023 add eax,dword ptr [ecx-8]
013C1026 add edx,dword ptr [ecx-4]
013C1029 add esi,dword ptr [ecx]
013C102B add edi,dword ptr [ecx+4]
013C102E add ecx,10h
013C1031 sub ebx,1
013C1034 jne arradd+23h (13C1023h)
013C1036 add edi,esi
013C1038 add edi,edx
013C103A add eax,edi
013C103C sub dword ptr [esp+10h],1
013C1041 jne arradd+16h (13C1016h)
013C1043 pop edi
013C1044 pop esi
013C1045 pop ebp
013C1046 pop ebx
Run Code Online (Sandbox Code Playgroud)
ecx
指向数组,我们可以看到内部循环在这里展开了 x4add
- 请注意来自以下地址的四个连续指令,并且ecx
在循环内一次前进 16 个字节(4 个字)。
对于成员函数的未优化版本,doadd
:
int tester::doadd()
{
sm = 0;
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
{
sm += arr[i];
}
}
return sm;
}
Run Code Online (Sandbox Code Playgroud)
反汇编是(由于编译器将其内联到 中,因此很难找到main
):
int tr_result = tr.doadd();
013C114A xor edi,edi
013C114C lea ecx,[edi+0Ah]
013C114F nop
013C1150 xor eax,eax
013C1152 add edi,dword ptr [esi+eax*4]
013C1155 inc eax
013C1156 cmp eax,0A6E49C0h
013C115B jl main+102h (13C1152h)
013C115D sub ecx,1
013C1160 jne main+100h (13C1150h)
Run Code Online (Sandbox Code Playgroud)
注意 2 件事:
edi
。因此,这里没有“小心”别名。的值sm
不会一直被重新读取。edi
仅初始化一次,然后用作临时的。您看不到它的返回值,因为编译器对其进行了优化并edi
直接用作内联代码的返回值。最后,这是成员函数的“优化”版本,mysm
手动将总和保留在本地:
int tester::doadd_opt()
{
sm = 0;
int mysm = 0;
for (int n = 0; n < 10; ++n)
{
for (int i = 0; i < sz; ++i)
{
mysm += arr[i];
}
}
sm = mysm;
return sm;
}
Run Code Online (Sandbox Code Playgroud)
(再次,内联)反汇编是:
int tr_result_opt = tr_opt.doadd_opt();
013C11F6 xor edi,edi
013C11F8 lea ebp,[edi+0Ah]
013C11FB jmp main+1B0h (13C1200h)
013C11FD lea ecx,[ecx]
013C1200 xor ecx,ecx
013C1202 xor edx,edx
013C1204 xor eax,eax
013C1206 add ecx,dword ptr [esi+eax*4]
013C1209 add edx,dword ptr [esi+eax*4+4]
013C120D add eax,2
013C1210 cmp eax,0A6E49BFh
013C1215 jl main+1B6h (13C1206h)
013C1217 cmp eax,0A6E49C0h
013C121C jge main+1D1h (13C1221h)
013C121E add edi,dword ptr [esi+eax*4]
013C1221 add ecx,edx
013C1223 add edi,ecx
013C1225 sub ebp,1
013C1228 jne main+1B0h (13C1200h)
Run Code Online (Sandbox Code Playgroud)
这里的循环已展开,但只是 x2。
这很好地解释了我对速度差的观察。对于 175e6 阵列,该函数运行约 1.2 秒,未优化的成员约 1.5 秒,优化的成员约 1.3 秒。(请注意,这对您来说可能有所不同,在另一台机器上,我获得了所有 3 个版本的更接近的运行时间)。
海湾合作委员会呢?当用它编译时,所有 3 个版本的运行时间约为 1.5 秒。怀疑缺乏展开,我查看了gcc
反汇编,确实:gcc 没有展开任何版本。