为什么我应该使用归约而不是原子变量?
Why should I use a reduction rather than an atomic variable?
假设我们想在 OpenMP 循环中计算一些东西。比较减少
int counter = 0;
#pragma omp for reduction( + : counter )
for (...) {
...
counter++;
}
与原子增量
int counter = 0;
#pragma omp for
for (...) {
...
#pragma omp atomic
counter++
}
原子访问立即提供结果,而归约只在循环结束时假定其正确值。例如,减少不允许这样:
int t = counter;
if (t % 1000 == 0) {
printf ("%dk iterations\n", t/1000);
}
因此提供的功能较少。
为什么我要使用归约而不是对计数器的原子访问?
简答:
性能
长答案:
因为原子变量是有代价的,这个代价就是同步。
为了确保不存在竞争条件,即两个线程同时修改同一个变量,线程必须 synchronize 这实际上意味着你失去了并行性,即线程是 连载.
另一方面,归约是一种通用操作,可以使用并行归约算法并行执行。
阅读 this and 篇文章,了解有关并行缩减算法的更多信息。
附录:了解并行缩减的工作原理
想象一个场景,您有 4
个线程,并且您想要减少 8
个元素数组 A。您可以通过 3 个步骤完成此操作(查看附加图像以获得更好的感觉我在说什么):
- 步骤 0。索引为
i<4
的线程负责求和 A[i]=A[i]+A[i+4]
. 的结果
- 步骤 1。索引为
i<2
的线程负责求和 A[i]=A[i]+A[i+4/2]
. 的结果
- 步骤 2。索引为
i<4/4
的线程负责求和 A[i]=A[i]+A[i+4/4]
的结果
在此过程结束时,您将得到减少 A
第一个元素的结果,即 A[0]
性能是关键。
考虑以下程序
#include <stdio.h>
#include <omp.h>
#define N 1000000
int a[N], sum;
int main(){
double begin, end;
begin=omp_get_wtime();
for(int i =0; i<N; i++)
sum+=a[i];
end=omp_get_wtime();
printf("serial %g\t",end-begin);
begin=omp_get_wtime();
# pragma omp parallel for
for(int i =0; i<N; i++)
# pragma omp atomic
sum+=a[i];
end=omp_get_wtime();
printf("atomic %g\t",end-begin);
begin=omp_get_wtime();
# pragma omp parallel for reduction(+:sum)
for(int i =0; i<N; i++)
sum+=a[i];
end=omp_get_wtime();
printf("reduction %g\n",end-begin);
}
执行 (gcc -O3 -fopenmp) 时,它给出:
序列 0.00491182 原子 0.0786559 缩减 0.001103
所以大约 atomic=20xserial=80xreduction
'reduction' 适当地利用了并行性,使用 4 核计算机,我们可以获得 3--6 性能提升 vs "serial".
现在,"atomic" 比 "serial" 长 20 倍。正如前面的回答中所解释的,不仅内存访问的序列化禁用了并行性,而且所有内存访问都是由原子操作完成的。这些操作在现代计算机上至少需要 20--50 个周期,如果大量使用,将大大降低您的性能。
假设我们想在 OpenMP 循环中计算一些东西。比较减少
int counter = 0;
#pragma omp for reduction( + : counter )
for (...) {
...
counter++;
}
与原子增量
int counter = 0;
#pragma omp for
for (...) {
...
#pragma omp atomic
counter++
}
原子访问立即提供结果,而归约只在循环结束时假定其正确值。例如,减少不允许这样:
int t = counter;
if (t % 1000 == 0) {
printf ("%dk iterations\n", t/1000);
}
因此提供的功能较少。
为什么我要使用归约而不是对计数器的原子访问?
简答:
性能
长答案:
因为原子变量是有代价的,这个代价就是同步。 为了确保不存在竞争条件,即两个线程同时修改同一个变量,线程必须 synchronize 这实际上意味着你失去了并行性,即线程是 连载.
另一方面,归约是一种通用操作,可以使用并行归约算法并行执行。
阅读 this and
附录:了解并行缩减的工作原理
想象一个场景,您有 4
个线程,并且您想要减少 8
个元素数组 A。您可以通过 3 个步骤完成此操作(查看附加图像以获得更好的感觉我在说什么):
- 步骤 0。索引为
i<4
的线程负责求和A[i]=A[i]+A[i+4]
. 的结果
- 步骤 1。索引为
i<2
的线程负责求和A[i]=A[i]+A[i+4/2]
. 的结果
- 步骤 2。索引为
i<4/4
的线程负责求和A[i]=A[i]+A[i+4/4]
的结果
在此过程结束时,您将得到减少 A
第一个元素的结果,即 A[0]
性能是关键。
考虑以下程序
#include <stdio.h>
#include <omp.h>
#define N 1000000
int a[N], sum;
int main(){
double begin, end;
begin=omp_get_wtime();
for(int i =0; i<N; i++)
sum+=a[i];
end=omp_get_wtime();
printf("serial %g\t",end-begin);
begin=omp_get_wtime();
# pragma omp parallel for
for(int i =0; i<N; i++)
# pragma omp atomic
sum+=a[i];
end=omp_get_wtime();
printf("atomic %g\t",end-begin);
begin=omp_get_wtime();
# pragma omp parallel for reduction(+:sum)
for(int i =0; i<N; i++)
sum+=a[i];
end=omp_get_wtime();
printf("reduction %g\n",end-begin);
}
执行 (gcc -O3 -fopenmp) 时,它给出:
序列 0.00491182 原子 0.0786559 缩减 0.001103
所以大约 atomic=20xserial=80xreduction
'reduction' 适当地利用了并行性,使用 4 核计算机,我们可以获得 3--6 性能提升 vs "serial".
现在,"atomic" 比 "serial" 长 20 倍。正如前面的回答中所解释的,不仅内存访问的序列化禁用了并行性,而且所有内存访问都是由原子操作完成的。这些操作在现代计算机上至少需要 20--50 个周期,如果大量使用,将大大降低您的性能。