OpenMP 并行循环比常规循环慢得多

OpenMP parallel loop much slower than regular loop

整个程序已经sh运行k了一个简单的测试:

    const int loops = 1e10;
    int j[4] = { 1, 2, 3, 4 };
    time_t time = std::time(nullptr);
    for (int i = 0; i < loops; i++) j[i % 4] += 2;
    std::cout << std::time(nullptr) - time << std::endl;

    int k[4] = { 1, 2, 3, 4 };
    omp_set_num_threads(4);
    time = std::time(nullptr);
#pragma omp parallel for
    for (int i = 0; i < loops; i++) k[omp_get_thread_num()] += 2;
    std::cout << std::time(nullptr) - time << std::endl;

在第一种情况下,循环 运行 大约需要 3 秒,在第二种情况下,结果不一致,可能需要 4 - 9 秒。两个循环 运行 启用一些优化(比如有利于速度和整个程序优化)更快,但第二个循环仍然慢得多。我尝试在循环末尾添加屏障并将数组明确指定为 shared,但这没有帮助。我设法使并行循环 运行 更快的唯一情况是使循环为空。可能是什么问题?

Windows10 x64,CPU英特尔酷睿 i5 10300H(4 核)

正如各种评论中已经指出的那样,问题的症结在于false sharing。确实,您的示例是可以对此进行试验的典型案例。但是,您的代码中也存在不少问题,例如:

  • 您可能会在 loops 变量以及所有 jk 表中看到溢出;
  • 你的计时器并不是最好的选择(诚然,在这种情况下,我的部分有点迂腐);
  • 你没有使用你计算的值,这让编译器完全忽略了各种计算;
  • 你的两个循环不等价,不会给出相同的结果;为了让它正确,我回到了你原来的 i%4 公式并添加了一个 schedule( static, 1) 子句。这不是正确的做法,但这只是为了在不使用正确的 reduction 子句的情况下获得预期结果。

然后我重写了您的示例,并增加了我认为可以更好地解决虚假共享问题的方法:使用 reduction 子句。

#include <iostream>
#include <omp.h>

int main() {
    const long long loops = 1e10;
    long long j[4] = { 1, 2, 3, 4 };
    double time = omp_get_wtime();
    for ( long long i = 0; i < loops; i++ ) {
         j[i % 4] += 2;
    }
    std::cout << "sequential: " << omp_get_wtime() - time << std::endl;

    time = omp_get_wtime();
    long long k[4] = { 1, 2, 3, 4 };
    #pragma omp parallel for num_threads( 4 ) schedule( static, 1 )
    for ( long long i = 0; i < loops; i++ ) {
        k[i%4] += 2;
    }
    std::cout << "false sharing: " << omp_get_wtime() - time << std::endl;

    time = omp_get_wtime();
    long long l[4] = { 1, 2, 3, 4 };
    #pragma omp parallel for num_threads( 4 ) reduction( +: l[0:4] )
    for ( long long i = 0; i < loops; i++ ) {
        l[i%4] += 2;
    }
    std::cout << "reduction: " << omp_get_wtime() - time << std::endl;

    bool a = j[0]==k[0] && j[1]==k[1] && j[2]==k[2] && j[3]==k[3];
    bool b = j[0]==l[0] && j[1]==l[1] && j[2]==l[2] && j[3]==l[3];
    std::cout << "sanity check: " << a << " " << b << std::endl;

    return 0;
}

编译和 运行 没有优化在我的笔记本电脑上给出:

$ g++ -O0 -fopenmp false.cc 
$ ./a.out 
sequential: 15.5384
false sharing: 47.1417
reduction: 4.7565
sanity check: 1 1

这就说明了 reduction 子句带来的改进。 现在,从编译器启用优化可以提供更缓和的画面:

$ g++ -O3 -fopenmp false.cc 
$ ./a.out 
sequential: 4.8414
false sharing: 4.10714
reduction: 2.10953
sanity check: 1 1

如果有的话,这表明编译器现在非常擅长避免大多数错误共享。实际上,对于您的初始(错误)k[omp_get_thread_num()],有无 reduction 子句没有时间差异,表明编译器能够避免该问题。