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

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

我正在编写一个 cfd 模拟,并希望并行化我的 ~10^5 循环(晶格大小),它是成员函数的一部分。 openMp 代码的实现很简单:我读取共享数组的条目,使用线程私有量进行计算,最后再次写入共享数组。在每个数组中,我只访问循环号的数组元素,所以我不希望出现竞争条件,也看不到任何刷新的理由。测试代码(并行部分)的加速,我发现除了一个 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;
    }
}

}

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

    如果每个项目都是 int
  1. 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

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

讨论

我们将从查看 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 循环提供了比矢量化更快的时序。

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

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

结论

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