并行程序中向量的算术运算符重载的最佳实践



问题上下文

我目前正在使用OpenMP并行编程模型分析和改进用C++编写的计算密集型并发应用程序的性能。我在评测工具中看到,代码的特定并行区域会将频率降至200MHz,这意味着在系统时间或CPU空闲的情况下会花费大量周期。我已经确定,导致此问题的原因是并发执行大量内存分配,导致分配器同步线程并浪费大量等待时间。

然而,这些内存分配是vector<double>运算符重载的结果,该运算符在所讨论的并行循环中大量使用(从现在起,感兴趣的区域(。操作员过载的功能如下:

std::vector<double> operator+( const std::vector<double>& v1 , const std::vector<double>& v2 )
{
std::vector<double> v = v1;
for( unsigned int i=0; i < v1.size() ; i++ )
{ v[i] += v2[i]; }
return v; 
}

这只是一个例子,因为其他算术运算符以及向量和标量(double类型(的运算也是如此。正如您所看到的,一个新的向量被初始化,然后作为操作的结果返回。这会导致至少一个malloc和一个空闲。但正如我所说,在感兴趣区域的一次迭代中,这会被调用多次,并且这个循环在大量并行线程上运行大量迭代(最多48个(。此操作调用的一个示例如下:

std::vector<double> corner_point= 0.5*(my_voxel_center+other_voxel_center);

在这种情况下,两个操作一个接一个地进行(向量的+,然后向量和标量的*(,然后将结果分配给新创建的向量。

问题

因此,我的问题如下:正如我们所看到的,这会有多糟糕,这应该是运算符重载的最佳实践,特别是在vector<>等类型上,以避免每次调用时分配和释放新向量?有更好的写法吗?

我读过";https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading"张贴搜索帮助,但没有关于重载函数中内存的具体使用,以及这些函数在并发应用程序中的执行方式的评论。

我知道,在这种情况下,也许没有其他办法:我应该把注意力集中在哪里来解决这个问题?我的想法是:

  • 使用另一个更好地处理并发的分配器
  • 根本不使用运算符重载来避免分配这些时间向量,并在每次出现在代码中时使用循环来执行操作。在这种情况下,代码大小的增长不应该像我所说的那样是一个问题,这个应用程序是计算关键的,这是最重要的事情

以下建议与运算符重载无关,通常与向量的使用有关。

有几点需要改进:

首先,在复制任何内容之前,您绝对应该使用reserve()

std::vector<double> operator+( const std::vector<double>& v1 , const std::vector<double>& v2 )
{
std::vector<double> v;
v.reserve(v1.size() + v2.size());
v = v1;
for( unsigned int i=0; i < v1.size() ; i++ )
{ v[i] += v2[i]; }
return v; 
}

这样,每个操作员调用将有1个分配,而不是几个(如果v1很小,v2很大,则至少有1个,最多有很多(。

然后我想insert()的范围版本可能比循环执行得更好,但这可能不是真的,应该进行测试(至少它不应该比循环差(。

std::vector<double> operator+( const std::vector<double>& v1 , const std::vector<double>& v2 )
{
std::vector<double> v;
v.reserve(v1.size() + v2.size());
v.insert(v.end(), v1.begin(), v1.end());
v.insert(v.end(), v2.begin(), v2.end());
return v; 
}

如果v1必须保持不变,也许也值得考虑。如果没有,那么可能值得过载operator +=并使用它


当然,一定要用不同的优化级别进行实验,有时-Os-O2可能比-O3更好

最新更新