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
变量以及所有 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
子句没有时间差异,表明编译器能够避免该问题。
整个程序已经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
变量以及所有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
子句没有时间差异,表明编译器能够避免该问题。