【问题标题】:How to properly use OpenMP?如何正确使用 OpenMP?
【发布时间】:2018-12-20 23:19:03
【问题描述】:

我正在构建一个应该将两个矩阵相乘的程序。问题是,我应该使用 OpenMP 并行化,但每次只使用一个“for”循环(首先是外部循环,然后是它的子循环,然后是内部循环),并且对于每种并行化方法,我应该在使用时分析结果不同数量的线程(1,2,4,8,16,32,64,128)。

我的问题是,我应该将 OpenMP 并行/私有部分放在哪里,以及哪些变量应该是私有/共享的?

// code to be parallelized using n_threads
omp_set_dynamic(0);     // Explicitly disable dynamic teams
omp_set_num_threads(n_threads);


#pragma omp parallel for shared(a, b, c) private(i,j)
for (i=0; i < TAM_MATRIZ; i++){
  for (j=0; j < TAM_MATRIZ; j++) {
    c[i][j] = 0; // initialize the result matrix with zeros
    for (k=0; k < TAM_MATRIZ; k++){
      #pragma omp atomic
      c[i][j] +=  a[i][k]*b[k][j];
    }
  }
  printf("Number of threads used: %d\n", omp_get_num_threads()); 
}

编辑

实际上,它是三个程序,第一个并行化外部循环,第二个并行化中间循环,最后一个并行化内部循环。 每个版本应该运行 8 次,使用指定的线程(1,2,4,8,16,32,64,128),然后我们应该比较不同程序版本和使用不同线程数的相同版本的性能。

我的疑问是在哪里共享或将其设为私有变量。并行化第一个循环时,应该共享哪些变量?当我处理第二个循环时,共享哪些变量?等等……

在我看来,我不能共享任何变量,因为我将有多个线程同时工作并且可以产生部分结果,但我知道我错了,我在这里问基本上是为了理解为什么。

【问题讨论】:

  • 您实际上可以检查或重复使用viennacl.sourceforge.net。它可以通过 GPU 加速,即 OpenCL 或 CUDA 或 OpenMP。
  • 感谢维克多·古宾的回复!事实上,这只是我大学科目的一个练习。这里的目的是学习如何使用 OpenMP 并将其应用到一个简单的代码示例中。
  • @AndréRocha(严重)并行化中间或内部循环并没有改变我回答中的数据共享建议。唯一的问题是,如果您并行化最内层循环(ijk-loop),则必须添加回原子(或巧妙的缩减)。

标签: c linux multithreading openmp matrix-multiplication


【解决方案1】:

你在正确的轨道上 - 这实际上很简单。

  • 您在private 子句中错过了k - 这会导致问题,因为在外部定义时默认为shared。最好的方法不是为每个变量显式选择数据共享,而是尽可能在本地声明变量(例如for (int ...),这几乎总是需要的,而且更容易推理。ab、@ 987654329@,来自外部,隐含shared - 循环变量在内部声明,隐含private

  • 幸运的是,不需要#pragma omp atomic。每个线程都在不同的i 上工作——因此没有两个线程可以尝试更新相同的c[i][j]。删除atomic 将大大提高性能。如果您需要原子,也可以考虑减少作为替代方案。

  • 如果你想打印omp_get_num_threads,你应该在循环之外,但在并行区域内。在您的情况下,这意味着您必须将omp parallel for 拆分为omp parallelomp for。使用omp single确保只有一个线程输出。

请注意,矩阵乘法的非常好的性能要复杂得多,超出了这个问题的范围。

编辑:

对于嵌套循环,如果可能的话,通常最好并行化最外层的循环 - 即没有阻止它的数据依赖性。在某些情况下,最外面的循环没有产生足够的并行度——在这些情况下,您宁愿使用collapse(2) 来并行化外部的 2 个循环。不要使用(parallel) for 两次,除非您完全知道自己在做什么。这样做的原因是并行化中间循环会产生更多更小的工作,这会增加相对开销。

在您的具体情况下,可以放心地假设TAM_MATRIZ &gt;&gt; n_threads0,这意味着最外层循环有足够的并行工作,可以让所有线程高效使用。

重申数据共享规则。对于普通的parallel 区域。

  • parallel 区域(和并行循环变量)的词法范围内定义的变量是隐式私有的。这些是您的线程工作的变量。如果变量仅在词法范围内使用,则始终1在最窄的词法范围内定义它。
  • 默认情况下,在词法范围之外定义的变量是隐式共享的。这些变量通常是并行区域的输入/输出-因此必须共享。确保避免数据竞争。

如果您遵循这一点,则几乎不需要显式定义private/shared 数据共享属性2

0 否则在这里使用 OpenMP 甚至没有意义。

1 异常适用于具有昂贵 ctor 的非平凡 C++ 类型。

2reduction / firstprivate 在显式使用时很有用。

【讨论】:

  • 感谢祖蓝的回复。好,我知道了 !但是在考虑其他并行化方法时,我仍然有疑问。我的意思是,要在外部循环上执行此操作,我应该按照您提到的那样执行,但是仅并行化中间循环呢?我如何知道哪些变量是共享的,哪些是私有的?我在哪里告诉 OpenMP 这是一个共享区域?
  • @AndréRocha 我编辑了答案以澄清这一点。注意:没有“共享区域”之类的东西,只有共享变量
  • 我添加了一个基于在 jk 上交换循环的答案。据我所知,您不想在我的情况下使用collapse(2)。在您的情况下,collapse(2) 可能是合适的。你怎么看?
  • 自从这个问题stackoverflow.com/a/51133851/2542702 以来,我一直在考虑行优先顺序与列优先顺序和依赖链。对 matrix*vector 使用列优先顺序会破坏依赖链,因此尽管您必须在内部循环上进行工作共享(与在外部循环上进行相比,这增加了一些开销),但它具有打破链的优势,我在我的回答中解释stackoverflow.com/a/51320608/2542702
  • @Zulan,是的,你对 OPs 问题的回答比我的好,所以我没有尝试复制你的答案。交换循环和打破依赖关系是最简单的优化,虽然它不能解决它仍然是内存带宽限制的问题,除非你使用循环平铺。
【解决方案2】:

您应该考虑像这样交换jk 循环的顺序

memset(c, 0, sizeof(c[0][0])*TAM_MATRIZ*TAM_MATRIZ);
#pragma omp parallel for
for (int i=0; i < TAM_MATRIZ; i++)
for (int k=0; k < TAM_MATRIZ; k++)
for (int j=0; j < TAM_MATRIZ; j++)
  c[i][j] +=  a[i][k]*b[k][j];

这可能比并行化具有更多的性能优势,因为它可以更好地使用缓存。但它还有一个更微妙的额外好处:它消除了求和的依赖链。

浮点数学不是关联的,这意味着 (a + b) +c 不一定等于 a + (b + c)。当您的内部循环经过k 时,每次迭代都会写入相同的变量c[i][j]。如果您更改求和的顺序(例如使用 SIMD),那么您可能会得到不同的结果。没有 OpenMP 或更松散的浮点模型,例如使用-Ofast,您的编译器(ICC 除外,默认情况下允许关联浮点运算)在存在浮点依赖时甚至可能不会使用 SIMD。

但是,当内部循环结束时 j 每次迭代 c[i][j] 写入不同的元素,因此依赖关系被打破。打破依赖关系不仅可以为您提供与线程数或 SIMD 宽度无关的一致结果,还可以通过更好的缓存使用来提高性能,并且即使在严格的浮点模型下也允许 SIMD。

【讨论】:

  • 谢谢@Z boson。但是如果我改变 i 和 j 循环的顺序,它会给我错误的计算,或者我只会颠倒计算顺序(列先于行)?
  • @AndréRocha 如果你交换ij 的顺序(i 是最里面的循环),那么你的代码将得到正确的答案,但速度很慢,因为它不会读/写到连续的内存地址,但每次读/写之间会有很大的步幅。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-08-13
  • 2011-01-24
相关资源
最近更新 更多