繁体   English   中英

openMP COLLAPSE 在内部是如何工作的?

[英]How does openMP COLLAPSE works internally?

我正在尝试 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;
        }

根据我的收集,折叠将循环“折叠”成一个大循环,然后在大循环中使用线程。 所以,对于前面的代码,我认为它相当于这样的东西:

#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. 是这样吗? 我还没有找到任何关于它如何“折叠”循环的解释。
  2. 如果是,使用它有什么好处? 它不会在不崩溃的情况下完全像并行性一样在 2 个线程之间划分作业吗? 如果没有,那么它是如何工作的?

PS:现在我想多了一点,如果 n 是奇数,比如 3,没有崩溃,一个线程将有 2 次迭代,而另一个只有 1 次。 这会导致线程的作业不均匀,并且效率会降低一些。 如果我们要使用我的崩溃等价物(如果崩溃确实是这样工作的话)每个线程将有“1.5”次迭代。 如果 n 非常大,那真的不重要,不是吗? 更不用说,这样做i= ij/n; j= mod(ij,n); i= ij/n; j= mod(ij,n); 每次,它都会降低性能,不是吗?

OpenMP 规范只是说(版本 4.5的第 58 页):

如果用大于 1 的参数值指定了collapse子句,则应用该子句的关联循环的迭代将折叠到一个更大的迭代空间中,然后根据schedule子句进行划分。 这些关联循环中迭代的顺序执行决定了折叠迭代空间中迭代的顺序。

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

例如,下面的代码:

#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;

由 GCC 的 OpenMP 引擎转换为:

<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>:

这是程序抽象语法树的类似 C 的表示,可能有点难以阅读,但它所做的是,它仅使用一次模运算来根据迭代的开始计算ij的初始值块 ( .istart0.3 ) 由调用GOMP_loop_static_start()决定。 然后它简单地增加ij就像人们期望实现循环嵌套一样,即增加j直到它达到100 ,然后将j重置为 0 并增加i 同时,它还保留了.iter.1中折叠迭代空间的当前迭代次数,基本上同时迭代了单个折叠循环和两个嵌套循环。

对于线程数不除以迭代次数的情况,OpenMP 标准说:

当没有指定chunk_size时,迭代空间被分成大小近似相等的块,每个线程最多分配一个块。 在这种情况下,块的大小是未指定的。

GCC 实现让具有最高 ID 的线程少执行一次迭代。 第 61 页的注释中概述了其他可能的分配策略。该列表绝不是详尽无遗的。

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

#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 所做的类似。 它确实避免了每次迭代的除法/模运算,但每个内部迭代器都会花费一个寄存器和加法。 当然对于不同的调度策略会有所不同。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM