openMP COLLAPSE如何在内部工作?

时间:2017-04-28 16:00:36

标签: c loops openmp matrix-multiplication collapse

我正在尝试openMP并行性,使用2个线程乘以2个矩阵。 我理解外循环并行是如何工作的(即没有" collapse(2)"工作)。

现在,使用崩溃。

#pragma omp parallel for collapse(2) num_threads(2)
    for( i = 0; i < m; i++)
        for( j = 0; j < n; j++)
        {
            s = 0;
            for( k = 0; k < p; k++)
                s += A[i][k] * B[k][j];
            C[i][j] = s;
        }

从我收集的东西,崩溃&#34;崩溃&#34;将循环转换为单个大循环,然后在大循环中使用线程。因此,对于之前的代码,我认为它等同于这样的代码:

#pragma omp parallel for num_threads(2)
for (ij = 0; ij <n*m; ij++)
{ 
    i= ij/n; 
    j= mod(ij,n);
    s = 0;
    for( k = 0; k < p; k++)
        s += A[i][k] * B[k][j];
    C[i][j] = s;
}

我的问题是:

  1. 这是怎么回事?我没有找到任何关于它的解释  &#34;折叠&#34;循环。
  2. 如果是,使用它有什么好处?没有按&#39;吨  它将两个线程之间的作业完全分开,就像没有并行性一样  倒塌?如果没有,那么它是如何工作的?
  3. PS:现在我正在考虑更多,如果n是奇数,比如3,没有崩溃,一个线程将有2次迭代,而另一个只有一次。这导致线程的作业不均匀,效率稍低。 如果我们使用我的崩溃等价物(如果崩溃确实有效),每个线程都会有&#34; 1.5&#34;迭代。如果n非常大,那真的不重要,不是吗?更不用说,每次都做i= ij/n; j= mod(ij,n);,它会降低性能,不是吗?

2 个答案:

答案 0 :(得分:2)

OpenMP规范只是说明了Version 4.5的第58页):

  

如果使用大于1的参数值指定collapse子句,则子句应用的关联循环的迭代将折叠为一个更大的迭代空间,然后根据{{1子句。在这些关联循环中顺序执行迭代确定了折叠迭代空间中迭代的顺序。

所以,基本上你的逻辑是正确的,除了你的代码等同于schedule的情况,即迭代块大小为1.在一般情况下,大多数OpenMP运行时的默认调度为schedule(static,1) collapse(2) ,这意味着块大小将(大约)等于迭代次数除以线程数。然后,编译器可以使用一些优化来实现它,例如,为外循环运行部分内循环以获得固定值,然后使用完整内循环运行整数个外迭代,然后再运行部分内循环。

例如,以下代码:

schedule(static)

由GCC的OpenMP引擎转换为:

#pragma omp parallel for collapse(2)
for (int i = 0; i < 100; i++)
    for (int j = 0; j < 100; j++)
        a[100*i+j] = i+j;

这是程序抽象语法树的类似C的表示,可能有点难以阅读,但它的作用是,它只使用模运算一次来计算<bb 3>: i = 0; j = 0; D.1626 = __builtin_GOMP_loop_static_start (0, 10000, 1, 0, &.istart0.3, &.iend0.4); if (D.1626 != 0) goto <bb 8>; else goto <bb 5>; <bb 8>: .iter.1 = .istart0.3; .iend0.5 = .iend0.4; .tem.6 = .iter.1; D.1630 = .tem.6 % 100; j = (int) D.1630; .tem.6 = .tem.6 / 100; D.1631 = .tem.6 % 100; i = (int) D.1631; <bb 4>: D.1632 = i * 100; D.1633 = D.1632 + j; D.1634 = (long unsigned int) D.1633; D.1635 = D.1634 * 4; D.1636 = .omp_data_i->a; D.1637 = D.1636 + D.1635; D.1638 = i + j; *D.1637 = D.1638; .iter.1 = .iter.1 + 1; if (.iter.1 < .iend0.5) goto <bb 10>; else goto <bb 9>; <bb 9>: D.1639 = __builtin_GOMP_loop_static_next (&.istart0.3, &.iend0.4); if (D.1639 != 0) goto <bb 8>; else goto <bb 5>; <bb 10>: j = j + 1; if (j <= 99) goto <bb 4>; else goto <bb 11>; <bb 11>: j = 0; i = i + 1; goto <bb 4>; <bb 5>: __builtin_GOMP_loop_end_nowait (); <bb 6>: 和{的初始值{1}}基于对i的调用确定的迭代块(j)的开始。然后它只会增加.istart0.3GOMP_loop_static_start(),因为人们会期望实现循环嵌套,即增加i直到达到j,然后将j重置为0并增加100。同时,它还保留j中折叠迭代空间的当前迭代次数,基本上同时迭代单个折叠循环和两个嵌套循环。

对于线程数没有划分迭代次数的情况,OpenMP标准说:

  

当没有指定 chunk_size 时,迭代空间被划分为大小大致相等的块,并且最多一个块被分配给每个线程。在这种情况下,未指定块的大小。

GCC实现使具有最高ID的线程执行少一次迭代。其他可能的分发策略在第61页的注释中列出。该列表并非详尽无遗。

答案 1 :(得分:0)

标准本身未指定确切的行为。但是,该标准要求内循环对外循环的每次迭代具有完全相同的迭代。这允许以下转换:

#pragma omp parallel
{
    int iter_total = m * n;
    int iter_per_thread = 1 + (iter_total - 1) / omp_num_threads(); // ceil
    int iter_start = iter_per_thread * omp_get_thread_num();
    int iter_end = min(iter_iter_start + iter_per_thread, iter_total);

    int ij = iter_start;
    for (int i = iter_start / n;; i++) {
        for (int j = iter_start % n; j < n; j++) {
            // normal loop body
            ij++;
            if (ij == iter_end) {
                goto end;
            }
        }
    }
    end:
}

通过略读反汇编,我相信这与GCC的相似。它确实避免了每次迭代除法/模数,但每个内部迭代器需要一个寄存器和加法。当然,不同的调度策略会有所不同。

折叠循环确实增加了可以分配给线程的循环迭代次数,从而有助于实现负载平衡,甚至可以首先暴露足够的并行工作。