前缀总和花费太长的OpenMP

时间:2014-01-25 16:02:06

标签: parallel-processing openmp

我在OpenMP中实现前缀总和问题,我似乎没有得到任何加速。实际上,并行实现比顺序实现花费的时间更长。

这是前缀sums的代码:

for (k = 1; k < n; k = kk) {
    kk = k << 1;

    #pragma omp parallel for 
    for (i = kk - 1; i < n; i += kk) {
        x[i] = x[i-k] + x[i];
    }
 }

for (k = k >> 1; k > 1; k = kk) {
    kk = k >> 1;

    #pragma omp parallel for
    for (i = k - 1; i < n - kk; i += k) {
        x[i + kk] = x[i] + x[i + kk];
    }
}

我使用gcc -fopenmp -O3 prefix_sums.c编译了这个。我得到的1 000 000个整数的结果是:

用于顺序实现(也使用-O3编译):

0.001132
0.000929
0.000872
0.000865
0.000842

用于并行实现(5个内核重新运行5次):

0.025851
0.005493
0.006327
0.007092
0.030720

有人可以解释我的问题是什么吗?实现提供了正确的输出,但为什么需要这么长时间?

谢谢。

2 个答案:

答案 0 :(得分:5)

前缀和可以与MIMD(例如使用OpenMP)和SIMD(例如使用SSE / AVX)并行。

使用OpenMP进行前缀总计会有点痛苦,但这并不算太糟糕。我已经详细了解了这个simd-prefix-sum-on-intel-cpuparallel-cumulative-prefix-sums-in-openmp-communicating-values-between-thread

编辑:您正在进行前缀(原位)。上面的链接不是就地(非原生境)。 我修改了代码(见下文),在你做的时候就地进行前缀和测试。你可能需要两个以上的物理内核才能看到任何好的东西。

基本上你是两次通过的。在第一遍中,你做部分和,然后在第二遍中你用每个部分和的常数校正部分和。第二遍将由良好的编译器(例如,使用GCC但不使用MSVC)进行矢量化。也可以在第一次传递时使用SIMD,但是我没有使用过的编译器会对其进行矢量化,所以你必须自己使用内在函数。

算法变为O(n),因此它很快变为内存绑定而不是计算绑定。这意味着对于仅适合L1高速缓存的阵列,OpenMP开销过大。在这种情况下,最好只使用SIMD(没有开销)。对于较大的阵列,您可以从SIMD和MIMD中受益,但在某些时候算法变为内存限制,并且它不比非并行算法快得多。

#include <stdio.h>
#include <omp.h>

void prefixsum_inplace(float *x, int N) {
    float *suma;
    #pragma omp parallel
    {
        const int ithread = omp_get_thread_num();
        const int nthreads = omp_get_num_threads();
        #pragma omp single
        {
            suma = new float[nthreads+1];
            suma[0] = 0;
        }
        float sum = 0;
        #pragma omp for schedule(static)
        for (int i=0; i<N; i++) {
            sum += x[i];
            x[i] = sum;
        }
        suma[ithread+1] = sum;
        #pragma omp barrier
        float offset = 0;
        for(int i=0; i<(ithread+1); i++) {
            offset += suma[i];
        }
        #pragma omp for schedule(static)
        for (int i=0; i<N; i++) {
            x[i] += offset;
        }
    }
    delete[] suma;
}

int main() {
    const int n = 20;
    float x[n];
    for(int i=0; i<n; i++) x[i] = 1.0*i;
    prefixsum_inplace(x, n);
    for(int i=0; i<n; i++) printf("%f %f\n", x[i], 0.5*i*(i+1));
}

答案 1 :(得分:0)

由于每个元素都依赖于前一个元素,因此您必须分两步拆分算法。每个线程只会在第一步中计算整数子集上的前缀(这样每个线程都不会依赖于任何其他线程)并且会添加其他相关线程的结果。

例如: x [3]取决于x [0],x [1],x [2]和x [3]。您可以在两个子集中拆分x [4]的计算。让一个线程通过加1和2来计算x [1],并让第二个线程将3和4加到x [4]中。在这一步之后,线程必须同步(如果你开始第二个并行循环,openMP会为你做什么),第二个线程将通过将x [2]添加到x [4]来计算最终答案。如果你有很多整数和许多线程,那么通过三个步骤分解计算甚至可能是有益的。

这基本上是并行缩减,可用于并行化大多数(?)迭代算法。在DrDobbs上,理论和一些图像是关于精确平行减少的。

Ps:仔细研究一下你的算法,看起来你实现的前缀和问题相当复杂。它仍然有很多依赖(我确实仔细检查过),但我认为我上面的陈述仍然有效,你可以平行减少。但我想知道:你是否实现了一个实际用于创建硬件电路的算法?