std::map 上的自定义 omp 减少

Posted

技术标签:

【中文标题】std::map 上的自定义 omp 减少【英文标题】:custom omp reduction on std::map's 【发布时间】:2019-11-25 14:31:38 【问题描述】:

所以我遇到的问题或多或少如下:

我有一组相对较大的数据,其中始终包含一对标识符和一个与之相关的值。因此,存在相对较少的不同但任意的标识符。

在 c++ 中,这可能看起来像 std::vector<std::pair<size_t, double> >

我现在想生成一个std::map,它告诉我们每个标识符的所有值的总和,因此在本例中为std::map<size_t, double>

所以对于

的输入
std::vector<std::pair<size_t, double>> typeDoubleVec1, 2., 3, 4., 1, 3., 3, 5., 2, 1.;

我想要一个等于:

std::map<size_t, double> result1, 5., 2, 1., 3, 9.

完成这项工作的函数如下所示。因此,第二个输入向量指定存在哪些标识符:

std::map<size_t, double> foo(const std::vector<std::pair<size_t, double>> &typeDoubleVec, const std::vector<size_t> &typeVec) 
  // create the map that contains our return values.
  std::map<size_t, double> mymap;

  // ensure that mymap contains all identifiers.
  for (auto &elem : typeVec) 
    mymap[elem];
  

  // iterate over the elements
  for (size_t i = 0; i < typeDoubleVec.size(); ++i) 
    mymap.at(typeDoubleVec[i].first) += typeDoubleVec[i].second;
  
  return mymap;

有谁知道如何使用 OpenMP 加快速度?我认为这种工作方式是您需要自定义 OpenMP 缩减?

【问题讨论】:

如果这不是一个实际问题,那么也许它不是......一个问题。您可以将其发布为答案,假设有一个可以回答的问题 更重要的是,我认为SO上已经存在一些类似的问题和答案。同样的东西写了好几次,甚至在不同的地方写得更糟,只会造成混乱。 @SteffenSeckler 仅供参考,这样做的方法是提出问题,然后自己回答。这使问题/答案可搜索,并有助于定义您要解决的问题。 (这里不清楚)。它还可以让您获得 Stack Overflow 成就lol 谢谢!我已将其拆分为 Q+A 【参考方案1】:

所以我自己找到了答案: 有两种方法可以做到这一点,一种是自定义缩减,另一种是关键部分。其中我目前推荐后者,主要是因为前者在当前的clang编译器上被破坏了(v9.0.0,修复已经在trunk/master中)。

OpenMP 并行用于自定义缩减

解决问题的第一种方法是使用 OpenMP 缩减,通常如下所示:

// this does not work for maps!
#pragma omp parallel for reduction(+: mymap)

这段代码将无法编译,因为没有为 std::map 定义内置缩减 +

相反,我们将不得不定义我们自己的归约。快速浏览一些 OpenMP 规范 (https://www.openmp.org/spec-html/5.0/openmpsu107.html) 会发现以下用于定义自定义缩减的语法:

#pragma omp declare reduction(reduction-identifier : typename-list : 
combiner) [initializer-clause] newline
reduction-identifier:这是我们可以为自定义缩减指定的名称。 typename-list:这是定义了此缩减的类型名称列表。对我们来说这是std::map&lt;size_t, double&gt; combiner:这是表达式,进行实际的归约。它将omp_inomp_out 作为输入,并将组合结果存储在omp_out 中。对于简单的+ 缩减,这是omp_out += omp_in。 initializer-clause:这是一个可选表达式,其格式应为initializer(expression)。如果它缺失,则归约变量的线程本地副本默认初始化。如果 is 存在,则表达式必须具有omp_priv = function-name(argument-list)omp_priv = initializer 形式。它也可能使用omp_orig,它对应于归约变量的初始值。 在编译指示的末尾需要一个换行符。

在这种情况下,我们希望将两个具有相同键的映射的值相加。这可以在这样的函数中完成:

void mapAdd(std::map<size_t, double> &inout, std::map<size_t, double> &in) 
  for (auto initer = in.begin(), outiter = inout.begin(); initer != in.end(); ++initer, ++outiter) 
    outiter->second += initer->second;
  

如前所述,线程局部变量通常是默认初始化的。 但是,对于std::map,不需要默认初始化。 相反,每个线程局部变量都应该使用已经存在的映射进行初始化。 这可以在初始化程序中指定,因此我们的编译指示如下所示:

#pragma omp declare reduction(                             \
                              mapAdd :                     \
                              std::map<size_t, double> :   \
                              mapAdd(omp_out, omp_in)      \
                             )                             \
                    initializer (omp_priv=omp_orig)

它可以用于上面的omp parallel for

#pragma omp parallel for reduction(mapAdd : mymap)
  for (size_t i = 0; i < typeDoubleVec.size(); ++i) 
    mymap.at(typeDoubleVec[i].first) += typeDoubleVec[i].second;
  

缺点

这不适用于当前的 clang 编译器。 它编译得很好,但它产生了一个分段错误,经过一番挖掘后,我发现这不是我的错,而是一个编译器错误,因为在当前的 gcc 和 intel 编译器上发现了相同的程序。

此外,当在模板函数内声明 OpenMP 缩减时,clang 编译器会出现问题(未定义引用),因为它不会实例化 OpenMP 缩减内所需的所有函数。

另请参阅以下问题:

clang:(已修复)使用自定义 openmp 缩减的段错误:https://bugs.llvm.org/show_bug.cgi?id=44134 clang:(已修复)openmp 的编译问题声明减少模板函数:https://bugs.llvm.org/show_bug.cgi?id=44133

在自定义缩减中使用 lambdas

标准中未指定在自定义 OpenMP 缩减中使用 lambda(根据 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60228)。 因此,我不建议这样做。 但是,它确实适用于当前的 intel 编译器,并且将适用于 clang 编译器的下一个版本(9.0.1 或 10)。 GCC 还不支持它(参见:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=60228)。

使用临界区

避免减少需要的一种方法是在非常基本的级别上重现它们,即我们为每个线程创建一个本地副本,然后在关键部分内手动累积结果。这样做的好处是更容易阅读,但比通过自定义缩减的解决方案要慢一些,因为没有实现扇入。

采用这种方法的解决方案如下所示:

template <class vectype, class typevectype>
std::map<size_t, double> foo(const vectype &typeDoubleVec,
                             const typevectype &typevec) 
  std::map<size_t, double> mymap;

  // ensure that mymap has all elements of myvec.
  for (auto &elem : typevec) 
Does anyone know how to speed this up with OpenMP? The way I see this working is that you need a custom OpenMP reduction?
    mymap[elem];
  

#pragma omp parallel default(none) shared(typeDoubleVec, mymap)
  
    // we create a local copy of mymap for each thread!
    auto localmap = mymap;
#pragma omp for
    // iterate over the vector and add them to the local map
    for (size_t i = 0; i < typeDoubleVec.size(); ++i) 
      localmap.at(typeDoubleVec[i].first) += typeDoubleVec[i].second;
    
#pragma omp critical
    // sum up the local map to the global map using a critical section
    mapAdd(mymap, localmap);
  
  return mymap;

代码

使用自定义缩减的代码可以在https://gist.github.com/SteffenSeckler/404c214bcccf506d261264672e2b9341找到

使用https://gist.github.com/SteffenSeckler/91943b881677f3cbe7b2d7d475471ee8的临界区的代码

附言

感谢您将其拆分为 Q+A 的反馈

【讨论】:

以上是关于std::map 上的自定义 omp 减少的主要内容,如果未能解决你的问题,请参考以下文章

std::map使用结构体自定义键值

std::set 插入器不尊重自定义比较器。 (可能的编译器错误?)

通过存档页面上的自定义字段查询自定义帖子类型

C++ std::map sort 如何按值排序 自定义比较函数 比较对象某个字段

resignFirstResponder 无法关闭我在 iPad 上的自定义键盘。为啥?

NavigationDrawer 上的自定义字体