【发布时间】:2016-06-11 01:41:24
【问题描述】:
给定n 部分和,可以在 log2 并行步骤中对所有部分和进行求和。例如,假设有八个线程和八个部分和:s0, s1, s2, s3, s4, s5, s6, s7。这可以在log2(8) = 3 这样的连续步骤中减少;
thread0 thread1 thread2 thread4
s0 += s1 s2 += s3 s4 += s5 s6 +=s7
s0 += s2 s4 += s6
s0 += s4
我想使用 OpenMP 执行此操作,但我不想使用 OpenMP 的 reduction 子句。我想出了一个解决方案,但我认为可以使用 OpenMP 的 task 子句找到更好的解决方案。
这比标量加法更通用。让我选择一个更有用的案例:数组缩减(有关数组缩减的更多信息,请参阅here、here 和 here)。
假设我想对数组a 进行数组缩减。下面是一些为每个线程并行填充私有数组的代码。
int bins = 20;
int a[bins];
int **at; // array of pointers to arrays
for(int i = 0; i<bins; i++) a[i] = 0;
#pragma omp parallel
{
#pragma omp single
at = (int**)malloc(sizeof *at * omp_get_num_threads());
at[omp_get_thread_num()] = (int*)malloc(sizeof **at * bins);
int a_private[bins];
//arbitrary function to fill the arrays for each thread
for(int i = 0; i<bins; i++) at[omp_get_thread_num()][i] = i + omp_get_thread_num();
}
此时我有一个指向每个线程的数组的指针数组。现在我想将所有这些数组加在一起并将最终总和写入a。这是我想出的解决方案。
#pragma omp parallel
{
int n = omp_get_num_threads();
for(int m=1; n>1; m*=2) {
int c = n%2;
n/=2;
#pragma omp for
for(int i = 0; i<n; i++) {
int *p1 = at[2*i*m], *p2 = at[2*i*m+m];
for(int j = 0; j<bins; j++) p1[j] += p2[j];
}
n+=c;
}
#pragma omp single
memcpy(a, at[0], sizeof *a*bins);
free(at[omp_get_thread_num()]);
#pragma omp single
free(at);
}
让我试着解释一下这段代码的作用。假设有八个线程。让我们定义+= 运算符来表示对数组求和。例如s0 += s1是
for(int i=0; i<bins; i++) s0[i] += s1[i]
那么这段代码就可以了
n thread0 thread1 thread2 thread4
4 s0 += s1 s2 += s3 s4 += s5 s6 +=s7
2 s0 += s2 s4 += s6
1 s0 += s4
但是这个代码并不像我想要的那样理想。
一个问题是有一些隐含的障碍需要所有线程同步。这些障碍不应该是必要的。第一个障碍是填充数组和进行归约之间。第二个障碍是在减少的#pragma omp for 声明中。但是我不能用这种方法使用nowait 子句来消除障碍。
另一个问题是有几个线程不需要使用。例如有八个线程。还原的第一步只需要四个线程,第二步两个线程,最后一步只需要一个线程。但是,此方法将涉及缩减中的所有八个线程。不过,其他线程无论如何都不会做太多事情,应该直接进入屏障并等待,所以这可能不是什么大问题。
我的直觉是使用 omp task 子句可以找到更好的方法。不幸的是,我对task 子句几乎没有经验,到目前为止我所做的所有努力都比我现在失败的效果更好。
有人可以建议一个更好的解决方案来减少对数时间,例如使用OpenMP 的task 子句?
我找到了解决障碍问题的方法。这会异步减少。唯一剩下的问题是它仍然将不参与减少的线程放入繁忙的循环中。此方法使用堆栈之类的东西在临界区(这是critical sections don't have implicit barriers 中的键之一)将指针推送到堆栈(但从不弹出)。堆栈是串行操作的,但并行减少。
这是一个工作示例。
#include <stdio.h>
#include <omp.h>
#include <stdlib.h>
#include <string.h>
void foo6() {
int nthreads = 13;
omp_set_num_threads(nthreads);
int bins= 21;
int a[bins];
int **at;
int m = 0;
int nsums = 0;
for(int i = 0; i<bins; i++) a[i] = 0;
#pragma omp parallel
{
int n = omp_get_num_threads();
int ithread = omp_get_thread_num();
#pragma omp single
at = (int**)malloc(sizeof *at * n * 2);
int* a_private = (int*)malloc(sizeof *a_private * bins);
//arbitrary fill function
for(int i = 0; i<bins; i++) a_private[i] = i + omp_get_thread_num();
#pragma omp critical (stack_section)
at[nsums++] = a_private;
while(nsums<2*n-2) {
int *p1, *p2;
char pop = 0;
#pragma omp critical (stack_section)
if((nsums-m)>1) p1 = at[m], p2 = at[m+1], m +=2, pop = 1;
if(pop) {
for(int i = 0; i<bins; i++) p1[i] += p2[i];
#pragma omp critical (stack_section)
at[nsums++] = p1;
}
}
#pragma omp barrier
#pragma omp single
memcpy(a, at[2*n-2], sizeof **at *bins);
free(a_private);
#pragma omp single
free(at);
}
for(int i = 0; i<bins; i++) printf("%d ", a[i]); puts("");
for(int i = 0; i<bins; i++) printf("%d ", (nthreads-1)*nthreads/2 +nthreads*i); puts("");
}
int main(void) {
foo6();
}
我仍然觉得使用不会将未使用的线程置于繁忙循环中的任务可能会找到更好的方法。
【问题讨论】:
-
为什么不想使用 OpenMP 缩减?
-
@Jeff,因为
reduction是一个黑盒子。因为我不知道它是如何工作的,甚至不知道是否使用了log(nthreads)缩减。因为reduction在操作不通勤时不起作用。因为我认为知道如何“手工”做事很有用。因为我认为 OpenMP 是教授并行编程概念的一个很好的范例。 -
您是否阅读过规范或任何 OSS 运行时(在 GCC 和 Clang 或 Pathscale 中)?如果您拒绝打开盖子,它只是一个黑匣子。
-
OpenMP 应该实现实现者已知的最快缩减。我希望很多都是log(N)。您是否可以在测量中看到这一点取决于您如何构建它们。如果不摊销并行区域成本,许多实验将主要受内存成本或运行时开销的影响。
-
@IwillnotexistIdonotexist,通常是
n >> N,所以第二阶段如何做并不重要,因为时间完全由第一阶段主导。但是如果n ≈ N呢?在这种情况下,第二阶段将不是微不足道的。我承认我应该想出一个例子来说明这一点(我的意思是时间),但 OpenMP 的 SO 上的每个人都说使用reduction子句,因为它可能会在log(t)操作中执行第二阶段。所以我认为这可能是一个例子。
标签: c algorithm parallel-processing openmp reduce