openMp:调用动态数组的共享引用时性能严重损失

openMp: severe perfomance loss when calling shared references of dynamic arrays

本文关键字:性能 损失 引用 共享 调用 动态 数组 openMp      更新时间:2023-10-16

我正在编写一个 cfd 模拟,并希望并行化我的 ~10^5 循环(点阵大小),这是成员函数的一部分。openMp 代码的实现很简单:我读取共享数组的条目,使用线程私有数量进行计算,最后再次写入共享数组。在每个数组中,我只访问循环号的数组元素,所以我不希望出现竞争条件,也看不到任何刷新的理由。测试代码的加速(并行部分),我发现除了一个 CPU 之外,所有 CPU 都只以 ~70% 的速度运行。有人知道如何改善这一点吗?

void class::funcPar(bool parallel){
#pragma omp parallel
{
    int one, two, three;
    double four, five;
    #pragma omp for
    for(int b=0; b<lenAr; b++){
        one = A[b]+B[b];
        C[b] = one;
        one += D[b];
        E[b] = one;
    }
}

}

几点,然后测试代码,然后讨论:

  1. 如果每个项目都是int,则 10^5 并不多。启动多个线程所产生的开销可能大于好处。
  2. 使用 OMP 时,编译器优化可能会搞砸。
  3. 当每组内存处理少量操作时,循环可能会受到内存限制(即 CPU 花费时间等待请求的内存交付)

正如承诺的那样,这是代码:

#include <iostream>
#include <chrono>
#include <Eigen/Core>

Eigen::VectorXi A;
Eigen::VectorXi B;
Eigen::VectorXi D;
Eigen::VectorXi C;
Eigen::VectorXi E;
int size;
void regular()
{
    //#pragma omp parallel
    {
        int one;
//      #pragma omp for
        for(int b=0; b<size; b++){
            one = A[b]+B[b];
            C[b] = one;
            one += D[b];
            E[b] = one;
        }
    }
}
void parallel()
{
#pragma omp parallel
    {
        int one;
        #pragma omp for
        for(int b=0; b<size; b++){
            one = A[b]+B[b];
            C[b] = one;
            one += D[b];
            E[b] = one;
        }
    }
}
void vectorized()
{
    C = A+B;
    E = C+D;
}
void both()
{
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        int vals = size / nthreads;
        int startInd = tid * vals;
        if(tid == nthreads - 1)
            vals += size - nthreads * vals;
        auto am = Eigen::Map<Eigen::VectorXi>(A.data() + startInd, vals);
        auto bm = Eigen::Map<Eigen::VectorXi>(B.data() + startInd, vals);
        auto cm = Eigen::Map<Eigen::VectorXi>(C.data() + startInd, vals);
        auto dm = Eigen::Map<Eigen::VectorXi>(D.data() + startInd, vals);
        auto em = Eigen::Map<Eigen::VectorXi>(E.data() + startInd, vals);
        cm = am+bm;
        em = cm+dm;
    }
}
int main(int argc, char* argv[])
{
    srand(time(NULL));
    size = 100000;
    int iterations = 10;
    if(argc > 1)
        size = atoi(argv[1]);
    if(argc > 2)
        iterations = atoi(argv[2]);
    std::cout << "Size: " << size << "n";
    A = Eigen::VectorXi::Random(size);
    B = Eigen::VectorXi::Random(size);
    D = Eigen::VectorXi::Random(size);
    C = Eigen::VectorXi::Zero(size);
    E = Eigen::VectorXi::Zero(size);
    auto startReg = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        regular();
    auto endReg = std::chrono::high_resolution_clock::now();
    std::cerr << C.sum() - E.sum() << "n";
    auto startPar = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        parallel();
    auto endPar = std::chrono::high_resolution_clock::now();
    std::cerr << C.sum() - E.sum() << "n";
    auto startVec = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        vectorized();
    auto endVec = std::chrono::high_resolution_clock::now();
    std::cerr << C.sum() - E.sum() << "n";
    auto startPVc = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        both();
    auto endPVc = std::chrono::high_resolution_clock::now();
    std::cerr << C.sum() - E.sum() << "n";
    std::cout << "Timings:n";
    std::cout << "Regular:    " << std::chrono::duration_cast<std::chrono::microseconds>(endReg - startReg).count() / iterations << "n";
    std::cout << "Parallel:   " << std::chrono::duration_cast<std::chrono::microseconds>(endPar - startPar).count() / iterations << "n";
    std::cout << "Vectorized: " << std::chrono::duration_cast<std::chrono::microseconds>(endVec - startVec).count() / iterations << "n";
    std::cout << "Both      : " << std::chrono::duration_cast<std::chrono::microseconds>(endPVc - startPVc).count() / iterations << "n";
    return 0;
}

我使用 Eigen 作为向量库来帮助证明一个点 re:optimizations,我很快就会达到这个目标。代码以四种不同的优化模式编译:

g++ -fopenmp -std=c++11 -wall -pedantic -pthread -I C:\usr\include source.cpp -o a.exe

g++ -fopenmp -std=c++11 -wall -pedantic -pthread -O1 -I C:\usr\include source.cpp -o aO1.exe

g++ -fopenmp -std=c++11 -wall -pedantic -pthread -O2 -I C:\usr\include source.cpp -o aO2.exe

g++ -fopenmp -std=c++11 -wall -pedantic -pthread -O3 -I C:\usr\include source.cpp -o aO3.exe

在Windows下使用g++(x86_64-posix-sjlj,由 strawberryperl.com 项目构建)4.8.3。

讨论

我们将首先查看 10^5 与 10^6 个元素,在没有优化的情况下平均 100 次。

a 10^5(无优化):

Timings:
Regular:    9300
Parallel:   2620
Vectorized: 2170
Both      : 910

a 10^6(无优化):

Timings:
Regular:    93535
Parallel:   27191
Vectorized: 21831
Both      : 8600

矢量化 (SIMD) 在加速方面胜过 OMP。综合起来,我们会得到更好的时光。

移动到 -O1:

10^5:

Timings:
Regular:    780
Parallel:   300
Vectorized: 80
Both      : 80

10^6:

Timings:
Regular:    7340
Parallel:   2220
Vectorized: 1830
Both      : 1670

与没有优化相同,只是时间要好得多。

跳到 -O3:

10^5:

Timings:
Regular:    380
Parallel:   130
Vectorized: 80
Both      : 70

10^6:

Timings:
Regular:    3080
Parallel:   1750
Vectorized: 1810
Both      : 1680

对于 10^5,优化仍然胜过。但是,10^6 为 OMP 循环提供了比矢量化更快的时序。

在所有测试中,我们得到了大约 OMP 的 x2-x4 加速。

注意:我最初运行测试时,我有另一个使用所有内核的低优先级进程。出于某种原因,这主要影响并行测试,而不影响其他测试。确保正确计时。

结论

您的最小代码示例的行为与声明不同。更复杂的数据可能会出现内存访问模式等问题。添加足够的详细信息以准确重现您的问题 (MCVE) 以获得更好的帮助。