OpenMP/C++:并行 for 循环,之后缩减 - 最佳实践

OpenMP/C++: Parallel for loop with reduction afterwards - best practice?

本文关键字:最佳 之后 C++ 并行 循环 for OpenMP      更新时间:2023-10-16

给定以下代码...

for (size_t i = 0; i < clusters.size(); ++i)
{
    const std::set<int>& cluster = clusters[i];
    // ... expensive calculations ...
    for (int j : cluster)
        velocity[j] += f(j); 
} 

。我想在多个 CPU/内核上运行。函数f不使用velocity

第一个 for 循环之前的简单#pragma omp parallel for将产生不可预测/错误的结果,因为std::vector<T> velocity在内部循环中被修改。多个线程可以同时访问和(尝试)修改velocity的同一元素。

我认为第一个解决方案是在velocity[j] += f(j);操作之前编写#pragma omp atomic。这给了我一个编译错误(可能与元素的类型Eigen::Vector3dvelocity类成员有关)。此外,我读到与为每个线程都有一个私有变量并最终进行缩减相比,原子操作非常。所以这就是我想做的,我想。

我想出了这个:

#pragma omp parallel
{
    // these variables are local to each thread
    std::vector<Eigen::Vector3d> velocity_local(velocity.size());
    std::fill(velocity_local.begin(), velocity_local.end(), Eigen::Vector3d(0,0,0));
    #pragma omp for
    for (size_t i = 0; i < clusters.size(); ++i)
    {
        const std::set<int>& cluster = clusters[i];
        // ... expensive calculations ...
        for (int j : cluster)
            velocity_local[j] += f(j); // save results from the previous calculations
    } 
    // now each thread can save its results to the global variable
    #pragma omp critical
    {
        for (size_t i = 0; i < velocity_local.size(); ++i)
            velocity[i] += velocity_local[i];
    }
} 

这是一个很好的解决方案吗?这是最好的解决方案吗?(甚至正确吗?

进一步的想法:使用 reduce 子句(而不是critical部分)会引发编译器错误。我认为这是因为velocity是班级成员。

我试图找到一个有类似问题的问题,这个问题看起来几乎是一样的。但我认为我的情况可能会有所不同,因为最后一步包括一个for循环。此外,这是否是最佳方法的问题仍然存在。

编辑:根据每个评论的要求:reduction条款...

    #pragma omp parallel reduction(+:velocity)
    for (omp_int i = 0; i < velocity_local.size(); ++i)
        velocity[i] += velocity_local[i];

。引发以下错误:

错误 C3028:"形状匹配::速度":数据共享子句中只能使用变量或静态数据成员

(与g++类似的错误)

您正在执行数组缩减。我已经多次描述了这一点(例如,在 openmp 中减少数组并与 openmp 并行填充直方图数组缩减,而无需使用关键部分)。 您可以在有和没有关键部分的情况下执行此操作。

您已经使用关键部分(在您最近的编辑中)正确完成了这一点,所以让我描述如何在没有关键部分的情况下执行此操作。


std::vector<Eigen::Vector3d> velocitya;
#pragma omp parallel
{
    const int nthreads = omp_get_num_threads();
    const int ithread = omp_get_thread_num();
    const int vsize = velocity.size();
    #pragma omp single
    velocitya.resize(vsize*nthreads);
    std::fill(velocitya.begin()+vsize*ithread, velocitya.begin()+vsize*(ithread+1), 
              Eigen::Vector3d(0,0,0));
    #pragma omp for schedule(static)
    for (size_t i = 0; i < clusters.size(); i++) {
        const std::set<int>& cluster = clusters[i];
        // ... expensive calculations ...
        for (int j : cluster) velocitya[ithread*vsize+j] += f(j);
    } 
    #pragma omp for schedule(static)
    for(int i=0; i<vsize; i++) {
        for(int t=0; t<nthreads; t++) {
            velocity[i] += velocitya[vsize*t + i];
        }
    }
}

由于我没有做过的错误共享,此方法需要额外的小心/调整。

至于哪种方法更好,您必须进行测试。