如何正确使用OpenMP?

时间:2018-07-12 16:24:56

标签: c linux multithreading openmp matrix-multiplication

我正在构建一个应将两个矩阵相乘的程序。问题是,我应该使用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()); 
}

编辑

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

我的疑问在于共享或私有化它的位置。在并行化第一个循环时,应共享哪些变量?当我在第二个循环中工作时,共享哪些变量?等等...

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

2 个答案:

答案 0 :(得分:2)

您步入正轨-这实际上非常简单。

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

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

  • 如果要打印atomic,则应在循环之外但在并行区域内进行打印。就您而言,这意味着您必须将omp_get_num_threads分为omp parallel foromp parallel。使用omp for确保只有一个线程输出。

请注意,矩阵乘法带来的出色性能要复杂得多,超出了本问题的范围。

编辑:

对于嵌套循环,通常最好并行化最外面的循环-即,没有阻止它的数据依赖项。在某些情况下,最外面的循环无法产生足够的并行度-在这种情况下,您宁愿使用omp single来并行化外面的2个循环。除非您完全知道自己在做什么,否则不要两次使用collapse(2)。原因是并行化中间循环会产生更多的工作,从而增加了相对的开销。

在您的特定情况下,可以安全地假设(parallel) for 0 ,这意味着最外层的循环具有足够的并行工作量,可以有效地使用所有线程。

重申数据共享规则。对于正常的TAM_MATRIZ >> n_threads区域。

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

如果遵循此规则,几乎不需要显式定义parallel / private数据共享属性 2

0 否则,在这里使用OpenMP甚至毫无意义。

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

2 shared / reduction对于明确使用很有用。

答案 1 :(得分: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。