在c ++中优化对成员的访问

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成员.用本地版本替换所有其他版本仍然会使运行时变长,但是一旦我smmysm运行时替换就变得等于函数版本.


替代文字

解析度

对答案感到失望(抱歉的家伙),我摆脱了懒惰,潜入了这段代码的反汇编列表中.我在下面回答总结了调查结果.简而言之:它与别名无关,它与循环展开有关,并且在决定展开哪个循环时会应用一些奇怪的启发式MSVC.

Pau*_*l R 5

它可能是一个别名问题 - 编译器无法知道实例变量sm永远不会被指向arr,因此它必须将其sm视为有效的volatile,并将其保存在每次迭代中.您可以sm使用不同的类型来测试此假设.或者只使用一个临时的本地总和(它将被缓存在一个寄存器中)并sm在最后分配它.

  • @Eli:你没有提出汇编代码,虽然它可以提供帮助.我猜你甚至没看过它."为什么g ++优化它"谁说?也许g ++不会优化这两个函数,因此性能是相同的. (2认同)

Eli*_*sky 2

我用 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 没有展开任何版本