C 对数时间内的平行约化

C 对数时间内的平行约化,c,algorithm,parallel-processing,openmp,reduce,C,Algorithm,Parallel Processing,Openmp,Reduce,给定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的reduce子句。我提出

给定
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的
reduce
子句。我提出了一个解决方案,但我认为可以使用OpenMP的
task
子句找到更好的解决方案

这比标量加法更一般。让我选择一个更有用的案例:数组缩减(有关数组缩减的更多信息,请参阅和)

假设我想对数组
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();
}
让我试着解释一下这段代码的作用。让我们假设有八个线程。让我们定义
+=
操作符来表示数组上的求和。e、 g.
s0+=s1
is

for(int i=0; i<bins; i++) s0[i] += s1[i]
但这段代码并不像我希望的那样理想

一个问题是存在一些隐式障碍,要求所有线程同步。这些障碍不应该是必要的。第一个障碍是填充阵列和进行还原之间的障碍。第二个障碍是减少中的
声明的#pragma omp。但是我不能将
nowait
子句与此方法一起使用来移除障碍

另一个问题是有几个线程不需要使用。例如,使用八个线程。缩减的第一步只需要四个线程,第二步需要两个线程,最后一步只需要一个线程。但是,此方法将涉及缩减中的所有八个线程。尽管如此,其他线程无论如何都做不了多少工作,应该直接进入屏障等待,所以这可能不是什么大问题

我的直觉是,使用omp
task
子句可以找到更好的方法。不幸的是,我对
task
子句缺乏经验,迄今为止我在这方面的所有努力都比我现在失败的努力做得更好

有人能提出一个更好的解决方案来减少对数时间,例如使用OpenMP的
任务
子句吗


我找到了解决障碍问题的方法。这将异步地降低成本。剩下的唯一问题是,它仍然将不参与缩减的线程放入一个繁忙的循环中。此方法使用类似堆栈的东西将指针推送到关键部分的堆栈(但从不弹出它们)(这是as键之一。堆栈是串行操作的,但减少是并行的

下面是一个工作示例

#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();
}
#包括
#包括
#包括
#包括
void foo6(){
int=13;
omp_设置_数量_线程(n线程);
int bins=21;
INTA[bins];
int**at;
int m=0;
int nsums=0;

对于(inti=0;i实际上,使用递归分治方法在任务中清晰地实现这一点非常简单。这几乎就是代码

令人惊讶的是,这看起来并不太好:

顶部为
ICC16.0.2
,底部为
GCC5.3.0
,两者都带有
-O3

两者似乎都实现了序列化的缩减。我试图查看
gcc
/
libgomp
,但我并不清楚发生了什么。从中间代码/反汇编来看,它们似乎将最终合并包装在
GOMP_原子_开始
/
结束
——这似乎是一个全局互斥。Similarly
icc
将对
操作的调用封装在
kmpc\u critical
中。我想在昂贵的定制缩减操作中没有太多优化。传统的缩减可以通过硬件支持的原子操作来完成


请注意,每个
操作
都会更快,因为输入是在本地缓存的,但由于序列化,整体速度较慢。同样,这不是一个完美的比较,因为差异很大,早期的屏幕截图与
gcc
版本不同。但是趋势很明显,我也有缓存效果的数据。

为什么你不想使用OpenMP缩减吗?@Jeff,因为
缩减
是一个黑匣子。因为我不知道它是如何工作的,甚至不知道是否使用
日志(nthreads)
缩减。因为
缩减
在操作不通勤时不起作用。因为我认为知道如何“手工”做事很有用。因为我认为OpenMP是教授并行编程概念的一个很好的范例。你读过规范或任何OSS运行时(在GCC和Clang或Pathscale中)吗?如果你拒绝打开盖子,这只是一个黑盒子。OpenMP应该实现实现实现者已知的最快的缩减。我希望很多都是log(N)。你是否能在测量中看到这一点取决于你如何构造它们。如果你不摊销并行区域的成本,许多实验将主要由内存成本或运行时开销决定。@Iwillnotexistidnotexist,通常是
n>>n
,因此第二阶段如何进行并不重要,因为时间是完全不存在的第一阶段的预兆。但是如果
n呢≈ N
?在这种情况下,第二阶段并非无关紧要。我承认我本应该拿出一个例子来说明这一点(我的意思是计时),但OpenMP的每个人都说使用
reduce
子句,因为它可能在
log(t)中执行第二阶段
operations。因此,我认为这可能是一个例子。我测试了你的代码。它很有效!这正是我想要的答案。谢谢!这是一个教科书上的例子,这让它变得更好。我很高兴看到你能够提炼出我问题的本质,尽管有些含糊不清。这张图片太棒了!真的直观地显示我所看到的
n   thread0     thread1    thread2    thread4
4   s0 += s1    s2 += s3   s4 += s5   s6 +=s7
2   s0 += s2    s4 += s6
1   s0 += s4
#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();
}
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);
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);
        }