整个程序已缩减为一个简单的测试:
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 秒。在启用一些优化(例如有利于速度和整个程序优化)的情况下,两个循环运行得更快,但第二个循环仍然明显较慢。我尝试在循环末尾添加屏障并显式将数组指定为共享,但这没有帮助。我设法使并行循环运行得更快的唯一情况是使循环为空。可能是什么问题?
Windows 10 x64,CPU Intel Core i5 10300H(4 核)
最佳答案
正如各种评论中已经指出的那样,您问题的症结是 false sharing 。事实上,你的例子是一个可以进行实验的典型案例。但是,您的代码中也存在不少问题,例如:
- 您可能会在
loops
变量以及所有j
和k
表中看到溢出; - 你的计时器并不是真正的最佳选择(诚然,在这种情况下,我的观点有点迂腐);
- 您没有使用您计算的值,这使得编译器可以完全忽略各种计算;
- 你的两个循环不等价,不会给出相同的结果;为了使其正确,我回到原来的
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
子句没有时间差异,这表明编译器能够避免问题。
关于c++ - OpenMP 并行循环比常规循环慢得多,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71767545/