与嵌套for循环相比,C ++ 2D矩阵迭代器效率

时间:2012-06-29 16:22:20

标签: c++ arrays iterator 2d

我有一个2D主阵列的行主迭代器,derefence运算符如下:

int& Iterator::operator*(){ return matrix_[y_][x_]; }  //matrix_ has type int**

(前缀)增量运算符如下:

Iterator& Iterator::operator++()
{
    if((++x_ == xs_) && (++y_ != ys_)) //ys_, xs_ are the dimensions of the matrix
        x_ = 0;
    return *this;   
} 

我可以使用这个迭代器和std :: transform的优化版本(不返回不需要的结果,以保存一些指令)

template < class InputIterator, class OutputIterator, class UnaryOperator >
inline void MyTransform( InputIterator first1, InputIterator last1,
                         OutputIterator result, UnaryOperator op )
{
    for (; first1 != last1; ++first1, ++result)
        *result = op(*first1);
} 

这样称呼它:

MyTransform(matrix1.begin(),matrix1.end(),matrix2.begin(), MyFunctor());

然而,当我将性能与经典的嵌套for循环进行比较时:

MyFunctor() f;
for (int y=0; y<ySize; y++)
    for (int x=0; x<xSize; x++)
        matrix2.[y][x] = f(matrix1.[y][x]);

基于迭代器的解决方案是约。比嵌套的for-loop解决方案慢25%。这是MSVC和英特尔C ++编译器的情况(两者似乎都根据需要自动内联)。

现在问题似乎不是迭代器增量运算符,就好像我执行以下(丑陋)混合解决方案组合迭代器遍历和原始数组访问(后者使用迭代器的内部计数索引):

MyFunctor f;
for (; mat1Begin != mat1End; ++mat1Begin, ++mat2Begin)
{ 
    //mat1 and mat2 are type int**
    mat2[mat2Begin.y_][mat2Begin.x_] = f(mat1[mat1Begin.y_][mat1Begin.x_]);
}

它实际上比嵌套的for-loop解决方案快一点。这告诉我,在执行赋值时,性能命中是迭代器的解引用。

我的问题是,为什么在作业中取消引用迭代器

*result = op(*first1);
相对于原始阵列访问,

会产生如此巨大的性能损失?有没有什么技术可以用于这个简单的设计,以获得与原始阵列版本相当的性能(几乎)?

为了回应这个社区的有用反馈,我修改了代码,以便缓存循环的外部计数器,所以代码现在看起来如下:

int& Iterator::operator*()
{
    return column_[x_]; 
} 

Iterator& Iterator::operator++()
{
    if(++x_ == xs_)      //ys_, xs_ are the dimensions of the matrix
    {
        if(++y_ != ys_)
        { 
            x_ = 0;
            column_ = matrix_[y_];
        }
    }
    return *this;
} 

这将性能提升到英特尔C ++编译器的原始2D阵列性能的约85%,类似于MSVC编译器(实际上,在MSVC上调用MyTransform的速度较慢 - 生成的汇编指令要多得多 - 但让我们忽略它现在因为我对循环/解引用行为更感兴趣)。

当我将代码转换为使用指针算法(再次缓存列)时,性能明显差于英特尔编译器上的原始2D数组(~70%),但再次约为原始2D阵列的85%在MSVC编译器下

int& Image32Iterator::operator*()
{
    return *ptr_;
} 

//prefix
Image32Iterator& Image32Iterator::operator++()
{
    if(++ptr_ == ptrEnd_)
    {
        if(++row_ != rowEnd_)
        { 
            ptrEnd_ = (ptr_ = *row_) + xs_;
        }
    }
    return *this;
} 

所以我试图理解,使用基于迭代器的解决方案是否可以达到~85%的性能。令我感到惊讶的是,指针算术解决方案执行得更糟(因为我试图使用指针算法来判断我是否可以获得> 85%!)。

我将继续调查并更新发现,但欢迎任何见解......

...所以,重点关注为什么迭代器的指针算术版本对英特尔表现如此糟糕的问题,而它对MSVC编译器执行得很好,我看了一下程序集,问题似乎是在为循环生成的代码中。对于所有其他函数(即构造函数,迭代器和解引用运算符,不等运算符等),生成的代码对于Intel和MSVC几乎相同,如果有的话,它对于Intel来说稍微简洁一些)。

这是英特尔生成代码的汇编程序,后面是MSVC生成代码的汇编程序。我已经从for循环更改为while循环,以使生成的汇编程序更易于阅读:

英特尔生成的代码:

while(begin != end)
01392D31  push        eax  
01392D32  lea         eax,[begin]  
01392D35  lea         edx,[end]  
01392D38  mov         dword ptr [esp],edx  
01392D3B  mov         ecx,eax  
01392D3D  call        ImageExperiments::Image32Iterator::operator!= (139103Ch)  
01392D42  mov         byte ptr [ebp-74h],al  
01392D45  movzx       eax,byte ptr [ebp-74h]  
01392D49  movzx       eax,al  
01392D4C  test        eax,eax  
01392D4E  je          ImageExperiments::greyscale_iterator2+0BCh (1392DACh)  
{
    *it8 = gsf(*begin);
01392D50  lea         eax,[begin]  
01392D53  mov         ecx,eax  
01392D55  call        ImageExperiments::Image32Iterator::operator* (13910A5h)  
01392D5A  mov         dword ptr [ebp-10h],eax  
01392D5D  push        eax  
01392D5E  lea         eax,[gsf]  
01392D61  mov         edx,dword ptr [ebp-10h]  
01392D64  mov         edx,dword ptr [edx]  
01392D66  mov         dword ptr [esp],edx  
01392D69  mov         ecx,eax  
01392D6B  call        ImageExperiments::GreyScaleFunctor::operator() (139101Eh)  
01392D70  mov         byte ptr [ebp-72h],al  
01392D73  movzx       eax,byte ptr [ebp-72h]  
01392D77  mov         byte ptr [ebp-71h],al  
01392D7A  lea         eax,[it8]  
01392D7D  mov         ecx,eax  
01392D7F  call        ImageExperiments::Image8Iterator::operator* (1391050h)  
01392D84  mov         dword ptr [ebp-0Ch],eax  
01392D87  mov         eax,dword ptr [ebp-0Ch]  
01392D8A  movzx       edx,byte ptr [ebp-71h]  
01392D8E  mov         byte ptr [eax],dl  
    ++begin; 
01392D90  lea         eax,[begin]  
01392D93  mov         ecx,eax  
01392D95  call        ImageExperiments::Image32Iterator::operator++ (1391028h)  
01392D9A  mov         dword ptr [ebp-8],eax  
    ++it8;
01392D9D  lea         eax,[it8]  
01392DA0  mov         ecx,eax  
01392DA2  call        ImageExperiments::Image8Iterator::operator++ (1391014h)  
01392DA7  mov         dword ptr [ebp-4],eax  
01392DAA  jmp         ImageExperiments::greyscale_iterator2+41h (1392D31h)  
}
}
00CA2DAC  leave  
00CA2DAD  ret

MSVC生成的代码:

while(begin != end)
010316E0  lea         eax,[end]  
010316E3  push        eax  
010316E4  lea         ecx,[begin]  
010316E7  call        ImageExperiments::Image32Iterator::operator!= (1031096h)  
010316EC  movzx       ecx,al  
010316EF  test        ecx,ecx  
010316F1  je          ImageExperiments::greyscale_iterator2+74h (1031724h)  
{
    *it8 = gsf(*begin);
010316F3  lea         ecx,[begin]  
010316F6  call        ImageExperiments::Image32Iterator::operator* (10311EAh)  
010316FB  mov         eax,dword ptr [eax]  
010316FD  push        eax  
010316FE  lea         ecx,[gsf]  
01031701  call        ImageExperiments::GreyScaleFunctor::operator() (1031032h)  
01031706  mov         bl,al  
01031708  lea         ecx,[it8]  
0103170B  call        ImageExperiments::Image8Iterator::operator* (1031118h)  
01031710  mov         byte ptr [eax],bl  
    ++begin; 
01031712  lea         ecx,[begin]  
01031715  call        ImageExperiments::Image32Iterator::operator++ (1031041h)  
    ++it8;
0103171A  lea         ecx,[it8]  
0103171D  call        ImageExperiments::Image8Iterator::operator++ (103101Eh)  
}
01031722  jmp         ImageExperiments::greyscale_iterator2+30h (10316E0h)  
}
01031724  pop         edi  
01031725  pop         esi  
01031726  pop         ebx  
01031727  mov         esp,ebp  
01031729  pop         ebp  
0103172A  ret  

所以在我看来,英特尔编译器会产生大约。指令数量增加50%。我已经尝试用__restrict对指针进行限定,看看这对英特尔一代是否有任何影响,但事实并非如此。如果有人对于为什么英特尔编译器的循环代码如此笨重/慢速有任何建议,那么与MSVC ++编译器相比,我会非常感兴趣!

4 个答案:

答案 0 :(得分:2)

我已经开始重新创建代码,请参阅here

在g ++(4.6.3,-O3)下运行它,我发现:

1)非迭代器版本确实更快,但在我的情况下大约是4倍。  2)迭代器版本,无论你是否依赖迭代器或提取它们的计数器并使用它们直接访问数组,都是较慢的(按上述因素)。

我已经捕获了两个版本的汇编程序,并发现它们与版本2中与迭代器递增逻辑相关的大量代码完全不同。请注意,在这两种情况下都会内联所有内容。

案例1内部循环,没有迭代器:

.L18:
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L19:
    movq    24(%rsp), %rdx
    movq    40(%rsp), %rsi
    movq    (%rdx,%rcx), %rdx
    movq    (%rsi,%rcx), %rsi
    movl    (%rdx,%rax), %edx
    imull   %edx, %edx
    movl    %edx, (%rsi,%rax)
    addq    $4, %rax
    cmpq    $20000, %rax
    jne .L19
    addq    $8, %rcx
    cmpq    $40000, %rcx
    jne .L18
    movl    $.LC2, %esi
    movl    std::cout, %edi

案例2内循环,迭代器:

.L34:
    movl    %eax, 56(%rsp)
    movl    %ecx, 60(%rsp)
    movl    %edi, 72(%rsp)
    movl    %edi, 76(%rsp)
    movq    72(%rsp), %rdi
    cmpq    %rdi, 56(%rsp)
    je  .L36
    movq    24(%rsp), %rdi
    movslq  %eax, %r10
    movslq  %ecx, %r9
    movslq  %edx, %r11
    addl    $1, %eax
    movq    (%rdi,%r10,8), %rdi
    movslq  %esi, %r10
    movl    (%rdi,%r9,4), %edi
    movq    40(%rsp), %r9
    imull   %edi, %edi
    movq    (%r9,%r11,8), %r9
    movl    %edi, (%r9,%r10,4)
    movl    16(%rsp), %edi
    cmpl    %edi, %eax
    je  .L37
.L20:
    addl    $1, %edx
    cmpl    32(%rsp), %edx
    jne .L34
    addl    $1, %esi
    cmpl    %esi, %edx
    cmovne  %r8d, %edx
    jmp .L34
.L37:
    addl    $1, %ecx
    cmpl    %ecx, %eax
    cmovne  %r8d, %eax
    jmp .L20
.L36:

最终,我认为最好的建议是,如果您喜欢迭代器模式,则将矩阵内部数组重新定义为int*,允许迭代器成为指针周围的简单包装器。这显然是以矩阵的随机访问索引为代价的,该矩阵将需要处理int数组中给定xy和行宽的1-d偏移量(虽然不是火箭科学!)。

答案 1 :(得分:1)

我认为你的迭代器太大了。当您致电operator*()时,最糟糕的情况是您的编译器需要先获取y_x_,然后才能在matrix_获取x_的值,{{1 }}。我会尽可能尝试使用原始指针作为迭代器。这意味着当y_定义为matrix_时,您可以使用int matrix_[N][M]作为开始,将&matrix_[0][0]作为迭代结束。当然,总有&matrix_[N-1][M]

答案 2 :(得分:0)

1。 内存本地化。保证连续。 我注意到你澄清了变量mat1和mat2都是int **。但是如何在内存中处理matrix_。交互者只是可以想象到的任何地方。你的记忆是否为matrix_本地化? 基于堆的多维数组可能不是连续的。但是Vector&lt;&gt;是

这行代码不是使用实际的交互器,而是使用它们的变量来索引已本地化的数组。

mat2[mat2Begin.y_][mat2Begin.x_] = f(mat1[mat1Begin.y_][mat1Begin.x_]);

2。 你忘记了优化。 在使用递增运算符的第二种用法中,您在调用仿函数之前就已经执行了该步骤。

这可能意味着调用仿函数传递一个通过运算符解除引用的对象会干扰优化器优先排序的能力。

尝试在调用op()之前存储取消引用的对象,并查看是否可以消除成本。

for (; first1 != last1; ++first1, ++result)
{
    InputIterator::value_type val = *first1;
    *result = op(val);
}

我在参数赋值中使用运算符时看到了一些时髦的东西。直到调用之后延迟解析(发送表达式的其他解释,并在调用后解析表达式),并且不保证参数解析顺序。如果你有效率问题,最好通过参数发送实际的目标对象。

答案 3 :(得分:0)

您正在将双重间接matrix_[y_][x_]从函数调用中提升到循环中。可能编译器正在设法将指针matrix_[y_]缓存在一个案例中而不是另一个案例中;你能尝试在迭代器中缓存matrix_[y_]吗?