【问题标题】:Prefix sums taking too long OpenMP前缀和花费太长时间 OpenMP
【发布时间】:2014-02-16 15:16:09
【问题描述】:

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

这是我的前缀和代码:

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

对于并行实现(在 4 个内核上重新运行 5 次):

0.025851
0.005493
0.006327
0.007092
0.030720

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

谢谢。

【问题讨论】:

    标签: parallel-processing openmp


    【解决方案1】:

    MIMD(例如 OpenMP)和 SIMD(例如 SSE/AVX)的前缀总和可以并行。

    使用 OpenMP 进行前缀求和有点麻烦,但还不错。我已经详细介绍了这个simd-prefix-sum-on-intel-cpu 和这里parallel-cumulative-prefix-sums-in-openmp-communicating-values-between-thread

    编辑:您正在就地(原位)进行前缀总和。上面的链接不是就地(ex situ)。 我修改了代码(见下文)以在您正在做的地方进行前缀求和并对其进行测试。您可能需要两个以上的物理内核才能看到任何好处。

    基本上,您需要分两次执行此操作。在第一遍中,您进行部分总和,然后在第二遍中,您使用每个部分总和的常数来纠正部分总和。第二遍将由一个好的编译器向量化(例如,使用 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));
    }
    

    【讨论】:

      【解决方案2】:

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

      例如: 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:仔细检查您的算法,您似乎实现了相当复杂的前缀和问题。它仍然有很多依赖关系(我确实仔细检查过),但我认为我上面的陈述仍然有效,你可以并行减少。但我想知道:您是否实现了一种实际上是用于创建硬件电路的算法?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多