我有一个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 ++编译器相比,我会非常感兴趣!
答案 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
数组中给定x
,y
和行宽的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_]
吗?