我有一个二维数组的行主迭代器,具有如下的 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 循环解决方案慢 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 循环解决方案快一点。这向我表明,性能损失在于迭代器在执行分配时的取消引用。
我的问题是,为什么在分配中取消引用迭代器
*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 的调用速度较慢 - 生成了更多的汇编指令 - 但现在让我们忽略这一点,因为我我对循环/取消引用行为更感兴趣)。
当我将代码转换为使用指针算法(再次缓存列)时,性能明显比英特尔编译器上的原始二维数组(~70%)差,但在 MSVC 编译器下的原始二维数组又是~85%
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 来说更简洁一些)。
这是 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++ 编译器相比如此庞大/缓慢有任何建议,我会非常感兴趣!