前缀和的并行化 (Openmp)

Posted

技术标签:

【中文标题】前缀和的并行化 (Openmp)【英文标题】:Parallelization of a prefix sum (Openmp) 【发布时间】:2016-06-19 17:03:32 【问题描述】:

我有两个向量,a[n] 和 b[n],其中 n 是一个很大的数字。

a[0] = b[0];

for (i = 1; i < size; i++) 
        a[i] = a[i-1] + b[i];

通过这段代码,我们试图实现 a[i] 包含 b[] 中所有数字的总和,直到 b[i]。我需要使用 openmp 并行化这个循环。

主要问题是 a[i] 依赖于 a[i-1],所以我想到的唯一直接方法是等待每个 a[i-1] 数字准备好,这需要很多时间,没有任何意义。 openmp 中有什么方法可以解决这个问题吗?

【问题讨论】:

这是作业吗? 为什么你需要让它平行?对我来说似乎是一个顺序问题。如果您对各种向量进行并行处理,而不仅仅是a 主要问题是 a[i] 依赖于 a[i-1] 嗯,不,实际上不是。 a[i]sum(b[0]..b[i])。您已经勾勒出一种串行方式来计算它,但串行性不是计算的基本特征。将您最喜欢的搜索引擎指向并行前缀总和 @villintehaspam 不,不是,但我不知道这会如何改变这里的问题。 @Alex 我需要让它并行以尝试减少计算时间,因为 n 是一个非常大的数字。 【参考方案1】:

你是 18 世纪的卡尔·弗里德里希·高斯,你的小学老师决定用一个需要大量或重复算术的家庭作业来惩罚全班同学。在前一周,你的老师告诉你将前 100 个计数数字相加,因为你很聪明,所以你想出了with a quick solution。你的老师不喜欢这样,所以他提出了一个他认为无法优化的新问题。用你自己的符号你重写这个问题

a[0] = b[0];   
for (int i = 1; i < size; i++) a[i] = a[i-1] + b[i];

那你就明白了

a0  = b[0]
a1  = (b[0]) + b[1];
a2  = ((b[0]) + b[1]) + b[2]
a_n = b[0] + b[1] + b[2] + ... b[n]

再次使用您的符号,您将问题改写为

int sum = 0;
for (int i = 0; i < size; i++) sum += b[i], a[i] = sum;

如何优化这个?首先你定义

int sum(n0, n)  
    int sum = 0;
    for (int i = n0; i < n; i++) sum += b[i], a[i] = sum;
    return sum;

然后你意识到

a_n+1   = sum(0, n) + sum(n, n+1)
a_n+2   = sum(0, n) + sum(n, n+2)
a_n+m   = sum(0, n) + sum(n, n+m)
a_n+m+k = sum(0, n) + sum(n, n+m) + sum(n+m, n+m+k)

所以现在你知道该怎么做了。找t同学。让每个人处理数字的一个子集。为简单起见,您选择 size 是 100 和四个同学 t0, t1, t2, t3 然后每个人都这样做

 t0               t1                t2              t3
 s0 = sum(0,25)   s1 = sum(25,50)   s2 = sum(50,75) s3 = sum(75,100)

同时。然后定义

fix(int n0, int n, int offset) 
    for(int i=n0; i<n; i++) a[i] += offset

然后每个同学像这样再次同时返回他们的子集

t0             t1               t2                  t3 
fix(0, 25, 0)  fix(25, 50, s0)  fix(50, 75, s0+s1)  fix(75, 100, s0+s1+s2)

您意识到 t 同学花费大约相同的 K 秒来做算术,您可以在 2*K*size/t 秒内完成这项工作,而一个人需要 K*size 秒。很明显,您至少需要两个同学才能收支平衡。因此,与四个同学一起,他们应该以一个同学的一半时间完成。

现在你用你自己的符号写下你的算法

int *suma;  // array of partial results from each classmate
#pragma omp parallel

    int ithread = omp_get_thread_num();    //label of classmate
    int nthreads = omp_get_num_threads();  //number of classmates
    #pragma omp single
    suma = malloc(sizeof *suma * (nthreads+1)), suma[0] = 0;

    //now have each classmate calculate their partial result s = sum(n0, n)
    int s = 0;
    #pragma omp for schedule(static) nowait
    for (int i=0; i<size; i++) s += b[i], a[i] = sum;
    suma[ithread+1] = s;

    //now wait for each classmate to finish
    #pragma omp barrier

    // now each classmate sums each of the previous classmates results
    int offset = 0;
    for(int i=0; i<(ithread+1); i++) offset += suma[i];

    //now each classmates corrects their result 
    #pragma omp for schedule(static)
    for (int i=0; i<size; i++) a[i] += offset;

free(suma)

您意识到您可以优化每个同学必须添加前一个同学的结果的部分,但由于size &gt;&gt; t 您认为这不值得付出努力。

您的解决方案不如您添加计数的解决方案快,但是您的老师对他的几个学生比其他学生完成得早得多感到不高兴。所以现在他决定一个学生必须慢慢地向全班阅读b数组,当你报告结果a时,它也必须慢慢地完成。你称之为读/写带宽限制。 This severely limits the effectiveness of your algorithm.你现在要做什么?

The only thing you can think of is to get multiple classmates to read and record different subsets of the numbers to the class at the same time.

【讨论】:

不会同时读取和写入不同的数字子集仍然受读/写带宽限制吗?是否有一个技术名称,以便我可以进一步研究如何做到这一点?我发现程序的连续版本几乎是并行版本的两倍。这是因为受读/写带宽限制还是因为迭代整个大小大小的向量两次?谢谢你的回答,很有帮助。 @paraleljoe,当我几年前提出这个解决方案时,我并不理解内存带宽限制的含义。这是我认为您应该了解的有关并行计算的最重要概念。如果size 大于最后一级缓存,那么我的解决方案可能会更糟,所以这可能就是它变慢的原因。您可以改进它并分块执行并行部分,并将每个块的总和传递给下一个卡盘。但最好的办法是让混合并行版本与顺序版本一样快。 @paraleljoe,我的解决方案可能适用于多插槽系统/NUMA 系统上的大型size。所以每个插槽但不是每个核心。您也可以在分布式计算中使用它。对于单套接字系统,它只会对size 有用,不会很大。 @paraleljoe,我添加了一个链接到我的答案中,关于受内存带宽限制。我建议你读一下。顺便说一句,我有很多关于prefix sum 的答案,您可以阅读。我不声称自己是这方面的专家。就像我说的那样,在我理解内存带宽限制的真正含义之前,我就想出了这个解决方案。 @paraleljoe,您应该将size 的大小添加到您的问题中,以及为什么您需要为如此大的大小添加前缀总和。我不知道为什么你需要为一个非常大的size 做前缀总和。您可能可以将前缀和与其他计算一起分块进行,以尝试克服内存带宽。如果您需要更多帮助,您需要添加此信息。

以上是关于前缀和的并行化 (Openmp)的主要内容,如果未能解决你的问题,请参考以下文章

非for循环的OpenMP并行化

为啥 OpenMP 不并行化 vtk IntersectWithLine 代码

OpenMp实现并行化

在 C++ 中使用 OpenMP 并行化递归函数

混合组装和 Fortran 以及并行化 (OpenMP)

在 C++ 中使用 OpenMP 并行化算法