【问题标题】:Nested loop in OpenMP performance issueOpenMP 性能问题中的嵌套循环
【发布时间】:2016-12-01 12:43:49
【问题描述】:

我有这样一个信息量不足的嵌套循环(就像性能测试一样):

const int N = 300;

for (int num = 0; num < 10000; num++) {
    for (int i=0; i<N; i++) {
        for (int j=0; j<N; j++) {
        arr[i][j] = brr[i][j];
        crr[i][j] = arr[i][j] - brr[i][j];
        sum1 += crr[i][j];
        sum2 += arr[i][j];
        }
    }
}

经过的时间是

about 6 s

我尝试使用 OpenMP 并行化不同的循环。但我对我得到的结果感到非常困惑。

在第一步中,我只在第一个(最外层)循环中使用了“parallel for”编译指示:

#pragma omp parallel for schedule(static) reduction(+:sum1,sum2)
for (int num = 0; num < 10000; num++) {
        for (int i=0; i<N; i++) {
        for (int j=0; j<N; j++) {
        arr[i][j] = brr[i][j];
        crr[i][j] = arr[i][j] - brr[i][j];
        sum1 += crr[i][j];
        sum2 += arr[i][j];
        }
    }
}

经过的时间是(2个核心)

3.81 

然后我尝试使用“collapse”子句(2 个核心)并行化两个内部循环:

for (int num = 0; num < 10000; num++) {
     #pragma omp parallel for collapse(2) schedule(static) reduction(+:sum1, sum2)
     for (int i=0; i<N; i++) {
        for (int j=0; j<N; j++) {
        arr[i][j] = brr[i][j];
        crr[i][j] = arr[i][j] - brr[i][j];
        sum1 += crr[i][j];
        sum2 += arr[i][j];
        }
    }
}

经过的时间是

3.76

这比以前的情况要快。我不明白这是为什么。

如果我像这样使用这些内部循环的融合(这意味着在性能方面更好)

#pragma omp parallel for schedule(static) reduction(+:sum1,sum2)
 for (int n = 0; n < N * N; n++) {
     int i = n / N; int j = n % N;

经过的时间是

5.53

这让我很困惑。在这种情况下,性能会更差,但通常人们建议融合循环以获得更好的性能。

好的,现在让我们尝试只并行化像这样的中间循环(2 核):

for (int num = 0; num < 10000; num++) {
        #pragma omp parallel for schedule(static) reduction(+:sum1,sum2)
        for (int i=0; i<N; i++) {
        for (int j=0; j<N; j++) {
        arr[i][j] = brr[i][j];
        crr[i][j] = arr[i][j] - brr[i][j];
        sum1 += crr[i][j];
        sum2 += arr[i][j];
        }
    }
}

再次,性能变得更好:

3.703

最后一步 - 仅对最内层循环进行并行化(假设根据之前的结果这将是最快的情况)(2 个核心):

for (int num = 0; num < 10000; num++) {
        for (int i=0; i<N; i++) {
            #pragma omp parallel for schedule(static) reduction(+:sum1,sum2)
        for (int j=0; j<N; j++) {
        arr[i][j] = brr[i][j];
        crr[i][j] = arr[i][j] - brr[i][j];
        sum1 += crr[i][j];
        sum2 += arr[i][j];
        }
    }
}

但是(惊喜!)经过的时间是

about 11 s

这比以前的情况要慢得多。我无法理解这一切的原因。

顺便说一下,我正在寻找类似的问题,并找到了添加的建议

#pragma omp parallel

在第一个循环之前(例如,在thisthat 问题中)。但为什么它是正确的程序?如果我们把

#pragma omp parallel# 

for-loop 之前意味着每个线程都完全执行了for-loop,这是不正确的(过度工作)。确实,我尝试插入

#pragma omp parallel

在最外层循环之前有不同的位置

#pragma omp parallel for

正如我在这里描述的那样,在调用情况下性能更差(此外,在仅并行化最内层循环的最新情况下,答案也是不正确的(即,“sum2”不同 - 因为存在竞争条件)。

我想知道这种性能的原因(可能是数据交换的时间大于每个线程上实际计算的时间,但这是最新的情况)以及哪种解决方案最正确一个。

编辑:我已禁用编译器的优化(通过 $-O0$ 选项),结果仍然相同(除了最新示例中经过的时间(并行化最内层循环时)从 11 减少秒到 8 秒)。 编译器选项:

g++ -std=gnu++0x -fopenmp -O0 test.cpp

变量定义:

unsigned int seed;
const int N = 300;

int main()
{

double arr[N][N];
double brr[N][N];
for (int i=0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        arr[i][j] = i * j;
        brr[i][j] = i + j;
    }
}

double start = omp_get_wtime();
double crr[N][N];
double sum1 = 0;
double sum2 = 0;

【问题讨论】:

  • 编写代码只是为了测试性能非常困难。 arrcrr 赋值在第一次迭代后不会改变任何东西 - 所以编译器可能会优化它。如果他们这样做了,那么外部循环并行化将是不正确的。讨论这些特定的循环对于理解这些数组实际发生变化的循环没有多大帮助。另外,请附上minimal reproducible example。特别是缺少编译器选项、编译器版本和变量定义,这非常重要。
  • @Zulan 我已经禁用了编译器的优化(见编辑过的问题)并添加了变量的声明。

标签: performance for-loop parallel-processing nested openmp


【解决方案1】:

最后一步 - 仅对最内层循环进行并行化(假设根据之前的结果这将是最快的情况)(2 个核心)

但是(惊喜!)经过的时间是:

about 11 s

一点也不意外。并行块执行隐式屏障,甚至可以加入和创建线程(一些库可能使用线程池来降低线程创建的成本)。

最后,打开并行区域的成本很高。你应该尽可能少做几次。线程将同时并行运行外部循环,但是一旦它们到达omp for 块就会划分迭代空间,所以结果应该仍然正确(如果你不确定,你应该让你的程序检查这个) .

为了测试性能,您应该始终运行您的实验来进行编译器优化,因为它们会对应用程序的行为产生重大影响(您不应该对未优化程序的性能做出假设,因为它们的问题可能已经在优化期间得到解决) .

在创建包含所有循环的单个并行块时,执行时间在我的设置中减半(从使用 2 个线程的 9.536 秒开始,减少到 4.757 秒)。

omp for 块仍然应用隐式障碍,这在您的示例中不需要。在示例中添加nowait 子句可将执行时间缩短一半:2.120s。

从这里开始,您现在可以尝试探索其他选项。

由于更好​​地使用内存层次结构和向量化,并行化中间循环将执行时间减少到仅 0.732 秒。 L1 未命中率从 ~29% 降低到 ~0.3%。

在两个最里面的循环中使用折叠并没有什么大不了的,使用两个线程(应该检查强缩放)。

在这种情况下,使用 omp simd 等其他指令不会提高性能,因为编译器确信它可以安全地矢量化最内层循环。

#pragma omp parallel reduction(+:sum1,sum2)
for (int num = 0; num < 10000; num++) {
    #pragma omp for schedule(static) nowait
    for (int i=0; i<N; i++) {
        for (int j=0; j<N; j++) {
            arr[i][j] = brr[i][j];
            crr[i][j] = arr[i][j] - brr[i][j];
            sum1 += crr[i][j];
            sum2 += arr[i][j];
        }
    }
}

注意:使用 perf 计算的 L1 未命中率:

$  perf stat -e cache-references,cache-misses -r 3 ./test

【讨论】:

  • 非常感谢您的详细解答。顺便说一句,对我来说仍然存在一个问题:当创建包含所有循环的 omp 块并使用 omp for 并行化最内层循环时,会产生错误的结果。什么原因?..
  • 缓存未命中的原因是访问数组中的数据的方式。多次访问同一地址或访问彼此靠近的内存位置有助于利用memory hierarchy。由于how multidimensional arrays are defined in C,并行化第二个循环使线程访问内存的连续区域。具有应用程序性能改进用例的文章将帮助您了解此类情况。
  • 我试图重现错误的结果问题。就我而言,中间循环和最内层循环并行化的结果与顺序版本相同。
  • 好的,我明白了,谢谢。关于不同的结果:当我在所有循环之上使用#pragma omp parallel 并行化最内层循环时,每次都会得到不同的结果,例如 2.6924e+11、2.69104e+11。但是,当我在所有循环之上使用相同的#pragma omp 并行化中间循环时,我总是得到 2.691e+11。当我删除所有循环之上的#pragma omp parallel 时,这种差异就会消失。
  • @Jorge Bellon,你应该总是运行你的实验来优化编译器 - 你忘了on这个词吗? - 始终开启编译器优化! (?)
【解决方案2】:

由于并行编程中的变量在线程(内核)之间共享,您应该考虑processor cache-memory 的作用方式。此时您的代码可能会使用false-sharing 执行,这可能会损害您的处理器性能。

在您的第一个并行代码中,您在第一个 for 处调用 #pragma omp for,这意味着每个线程都有自己的 ij。与并行化for 的第二个并行代码的第二个和第三个(仅通过折叠区分)并行代码相比,这意味着每个i 都有自己的j。这两个代码更好,因为每个线程/核心更频繁地访问jcache-line。第 4 段代码对于缓存处理器来说完全是灾难,因为那里没有什么可以共享的。

我建议您使用英特尔的 PCM 或 PAPI 来衡量您的代码,以便找到合适的分析师。

问候。

【讨论】:

  • 谢谢。但是很多人告诉在所有嵌套循环之前使用 $pragma omp parallel$。在我的情况下,它会导致性能下降。
  • 这仅适用于某些情况。在这种情况下,您放置for (int num = 0; num &lt; 10000; num++) 循环只是为了流式传输工作负载。只是为了强调处理器以获得您的基准值。我对吗?如果是这样,您应该并行化内部循环,而不是外部第一个循环,因为 ij 是您的主要工作负载。
  • 是的,你是对的。如果我们有 3 个嵌套循环,我将最外层循环并行化只是为了进行一些比较。
猜你喜欢
  • 1970-01-01
  • 2014-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多