给定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&#39; 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();
}
我觉得可以找到一个更好的方法,使用的任务不会在繁忙的循环中使用线程。
答案 0 :(得分:11)
实际上,使用递归的分治方法完全实现任务非常简单。这几乎是textbook代码。
void operation(int* p1, int* p2, size_t bins)
{
for (int i = 0; i < bins; i++)
p1[i] += p2[i];
}
void reduce(int** arrs, size_t bins, int begin, int end)
{
assert(begin < end);
if (end - begin == 1) {
return;
}
int pivot = (begin + end) / 2;
/* Moving the termination condition here will avoid very short tasks,
* but make the code less nice. */
#pragma omp task
reduce(arrs, bins, begin, pivot);
#pragma omp task
reduce(arrs, bins, pivot, end);
#pragma omp taskwait
/* now begin and pivot contain the partial sums. */
operation(arrs[begin], arrs[pivot], bins);
}
/* call this within a parallel region */
#pragma omp single
reduce(at, bins, 0, n);
据我所知,没有不必要的同步,并且关键部分没有奇怪的轮询。它也可以自然地使用不同于您的排名数据的数据大小。我发现它非常干净,易于理解。所以我确实认为这比你的两个解决方案都更好。
但是让我们来看看它在实践中的表现*。为此,我们可以使用Score-p和Vampir:
* bins=10000
因此减少实际上需要一点时间。在没有涡轮增压的24核Haswell系统上执行。 gcc 4.8.4,-O3
。我在实际执行周围添加了一些缓冲区来隐藏初始化/后处理
图片显示应用程序中任何线程在水平时间轴上发生的情况。从上到下的树实现:
omp for
循环omp critical
有点任务。omp task
这很好地展示了具体实现如何实际执行。现在看来for循环实际上是最快的,尽管有不必要的同步。但是这种性能分析仍然存在许多缺陷。例如,我没有固定线程。在实践中,NUMA(非统一内存访问)非常重要:核心是否在它自己的套接字的自己的缓存/内存中有这些数据?这是任务解决方案变得不确定的地方。在简单比较中不考虑重复之间非常显着的差异。
如果减少操作在运行时变为可变,那么任务解决方案将比同步的for循环更好。
critical
解决方案有一些有趣的方面,被动线程不会持续等待,因此它们更有可能消耗CPU资源。这可能对性能有害,例如在turbo模式的情况下。
请记住,task
解决方案通过避免立即返回的生成任务而具有更多优化潜力。这些解决方案的执行方式也高度依赖于特定的OpenMP运行时。对于任务来说,英特尔的运行时似乎要糟糕得多。
我的建议是:
以下是来自this question的线性proccess_data_v1
的时间轴。
所以我想到了OpenMP减少。棘手的部分似乎是从循环内的at
数组获取数据而没有副本。我用NULL
初始化worker数组,并且只是第一次移动指针:
void meta_op(int** pp1, int* p2, size_t bins)
{
if (*pp1 == NULL) {
*pp1 = p2;
return;
}
operation(*pp1, p2, bins);
}
// ...
// declare before parallel region as global
int* awork = NULL;
#pragma omp declare reduction(merge : int* : meta_op(&omp_out, omp_in, 100000)) initializer (omp_priv=NULL)
#pragma omp for reduction(merge : awork)
for (int t = 0; t < n; t++) {
meta_op(&awork, at[t], bins);
}
令人惊讶的是,这看起来并不太好:
上方为icc 16.0.2
,下方为gcc 5.3.0
,均为-O3
。
两者似乎都实现了序列化的缩减。我试着调查gcc
/ libgomp
,但我不会立即明白发生了什么。从中间代码/反汇编,它们似乎将最终合并包装在GOMP_atomic_start
/ end
中 - 这似乎是一个全局互斥。同样,icc
将调用包含在operation
中的kmpc_critical
。我认为进行昂贵的自定义减少操作没有太多优化。传统的减少可以通过硬件支持的原子操作来完成。
注意每个operation
如何更快,因为输入是在本地缓存的,但由于序列化,它总体上更慢。同样,由于差异很大,这不是一个完美的比较,早期的截图是使用不同的gcc
版本。但趋势很明显,我也有关于缓存效果的数据。