【问题标题】:OpenMP parallelize grouped array sum using pointersOpenMP 使用指针并行化分组数组总和
【发布时间】:2022-01-07 16:56:41
【问题描述】:

我想在 C 中有效地并行化以下总和:

  #pragma omp parallel for num_threads(nth)
  for(int i = 0; i < l; ++i) pout[pg[i]] += px[i];

其中px 是指向包含一些数据的大小为l 的双精度数组x 的指针,pg 是指向大小为l 的整数数组g 的指针,它将每个数据点分配到x 指向以随机顺序出现的 ng 组之一,pout 是指向大小为 ng 的双精度数组 out 的指针,该数组用零初始化并包含求和 x 的结果超过g定义的分组。

上面的代码可以工作,但性能不是最优的,所以我想知道我是否可以在 OpenMP 中做一些事情(例如reduction() 子句)来改进执行。数组的尺寸lng,以及线程数nth 可供我使用并预先固定。我不能直接访问数组,只能将指针传递给执行并行求和的函数。

【问题讨论】:

  • 如果您的编译器中的 OMP 支持,则迭代组或对整个 pout 数组应用缩减
  • 您展示的间接寻址方式与迭代代码所获得的 cache-unfriendly 差不多;然后你向它抛出多个线程,以便它们都可以竞争共享缓存。得知单线程执行性能如此出色时,我不会感到惊讶。
  • 通过“上面的代码有效”,您的意思是没有 OpenMP pragma 的顺序实现,不是吗?否则,将是令人惊讶的。 lng 在实践中有多大?您预计大约使用多少个线程?最佳结果算法将根据这些值发生很大变化。
  • 感谢 cmets。代码是一些数据处理软件的内部组件,数组最多可以包含 1 亿个元素或更多。输入就像它们一样,重写整个事情以一次执行一组将是乏味的,而且不会更快。我是 C 编程 @HighPerformanceMark 的初学者,所以如果你能告诉我如何以更好的方式编写这个循环,我将不胜感激。就我的基准测试而言,使用指针并不比直接数组索引慢多少。并行执行使用 4 个线程可提供大约 2 倍的加速,因此不是最佳的。该软件在 PC 上运行。

标签: c openmp


【解决方案1】:
  1. 您的代码存在数据竞争(pout[pg[i]] += ... 行),您应该先修复它,然后再担心它的性能。

  2. 如果ng 不是太大并且您使用OpenMP 4.5+,最有效的解决方案是使用缩减:#pragma omp parallel for num_threads(nth) reduction(+:pout[:ng])

  3. 如果ng 太大,很可能最好的办法是在 PC 上使用该程序的串行版本。请注意,通过在pout[pg[i]] += .. 之前添加#pragma omp atomic,您的代码将是正确的,但其性能值得怀疑。

【讨论】:

  • 也非常感谢!我也会检查一下。
  • 所以我尝试了 1 亿个随机正态变量和 100 万个随机组。串行执行是 1.3 秒,两个线程执行大约 1 秒,4 个线程执行大约 0.8 秒。结果比我上面的结果在数值上更正确(确实存在数据竞争问题),但仍然存在小的数值问题(平均相对差异 1.4382e-06)。总的来说,我认为仅靠 OpenMP 并不能很好地解决问题,因此我将使用当前软件版本的串行代码,稍后再研究更详细的并行编程答案。
  • @Sebastian 并行化预计会有小的数值差异,因为浮点加法不是关联的,并且操作的顺序很重要。并不意味着并行化版本更差,它甚至可能更准确。如果它困扰您,您可以使用 Kahan 求和以获得更高的准确性,但这将花费大量运行时间。请参阅 docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html 了解著名的“每个计算机科学家应该了解的浮点运算知识”文档
  • 这只有在pout 被静态分配时才有效,对吧?不是malloced 左右?
  • 感谢@Homer512 的澄清,那么结果是合理的。 @VictorEijkhout pout 使用 memset(pout, 0.0, sizeof(double) * ng); 填充
【解决方案2】:

从您的描述看来,您的映射是多对少。这对于并行性来说是一个大问题,因为您可能在目标数组中存在写入冲突。尝试使用临界区或锁进行控制可能只会减慢代码速度。

除非它在内存中是禁止的,否则我会给每个线程一个pout 的私有副本并将其相加,然后将这些副本加在一起。现在源数组的读取可以在线程之间很好地划分。如果pout 数组不是太大,你的加速应该是不错的。

这是关键的代码:

#pragma omp parallel shared(sum,threadsum)
  {
    int thread = omp_get_thread_num(),
      myfirst = thread*ngroups;
    #pragma omp for
    for ( int i=0; i<biglen; i++ )
      threadsum[ myfirst+indexes[i] ] += 1;
    #pragma omp for
    for ( int igrp=0; igrp<ngroups; igrp++ )
      for ( int t=0; t<nthreads; t++ )
        sum[igrp] += threadsum[ t*ngroups+igrp ];
  }

现在是棘手的部分。我正在使用大小为 100M 的索引数组,但组数至关重要。使用 5000 个组时,我得到了很好的加速,但只有 50 个,即使我已经消除了虚假共享之类的东西,我得到的加速很可怜或没有加速。这一点我还不清楚。

最后一句话:我还编写了@Laci 仅使用缩减的解决方案。测试 1M 组输出:对于 2-8 个线程,归约解决方案实际上更快,但对于更高的线程数,我几乎赢了 2 倍,因为归约解决方案反复添加整个数组,而我只求和一次,然后在平行线。对于较少数量的组,总体而言,减少可能是首选。

【讨论】:

  • 谢谢!我明白你在说什么,这听起来对我来说是一个很好的解决方案。鉴于我是并行编程的新手,您是否可以给我一个基本模板来说明它的外观?
  • 非常感谢!我会尝试这个!
  • 您说输入中可以有 100M 个元素。组数的典型值是多少?
  • 我想知道 N=50 的性能不佳是否与存储转发惩罚有关。基本上,读取最近写入的值时会受到 3-5 个周期的惩罚。这自然有更高的机会发生在小 N 中,尽管我预计这会发生在小得多的 N 中。你的输入值是什么分布?它还可能与每个线程数组边界处的缓存行弹跳有关。当 N 是 8 的倍数时,这应该会消失。
  • @Homer512 我认为你的方法很有趣。基准测试取决于机器。我有 56 个具有大量带宽的核心双插槽节点。
猜你喜欢
  • 2015-01-19
  • 2021-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-10
  • 1970-01-01
相关资源
最近更新 更多