【问题标题】:Best practice on arithmetic operator overloading of vector on concurrent programs并发程序中向量算术运算符重载的最佳实践
【发布时间】:2023-03-07 19:40:01
【问题描述】:

问题背景

我目前正在使用 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 和一个 free。但正如我所说,这在感兴趣区域的一次迭代中被调用了多次,并且这个循环在大量并行线程(最多 48 个)上运行大量迭代。此操作调用的一个示例如下:

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

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

问题

所以,我的问题如下:正如我们已经看到它的表现有多糟糕,这应该是运算符重载的最佳实践,特别是对于像vector&lt;&gt; 这样的类型,以避免分配和释放每次调用一个新向量?有没有更好的写法?

我已阅读“https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading”帖子寻求帮助,但没有 cmets关于重载函数中内存的具体使用,以及这些函数在并发应用程序上的执行方式。

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

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

【问题讨论】:

    标签: c++ memory-management parallel-processing operator-overloading


    【解决方案1】:

    以下建议与运算符重载无关,而是与一般向量的使用有关。

    有几点需要改进:

    首先,在复制任何内容之前,您绝对应该使用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 个分配,而不是多个(至少 1 个,如果 v1 很小,v2 很大,则最多很多)。

    那么我猜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 更好

    【讨论】:

    • 感谢您的回答!我会注意到向量复制和分配方面的这些改进。但是我认为您误解了加法重载的含义。我想以数学方式添加两个向量:假设两个操作数的大小相同,将向量的每个元素与另一个向量中的对应元素相加。这就是for 循环的原因。
    • @Marc 啊,我的错。我弄错了。那我的回答是无效的,我会马上删除它,但你仍然可以考虑是否需要保持原始向量不变——如果你可以添加它而不是创建一个新的向量,它就不需要分配。此外,如果您可以在编译时指定大小,std::array&lt;double, N&gt; 比向量便宜得多(无分配)。
    猜你喜欢
    • 1970-01-01
    • 2015-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-10
    • 2012-12-06
    相关资源
    最近更新 更多