【问题标题】:Use OpenMP to find minimum for sets in parallel, C++使用 OpenMP 找到并行集合的最小值,C++
【发布时间】:2013-08-29 00:10:12
【问题描述】:

我正在 C++ 中实现 Boruvka 的算法来找到图的最小生成树。该算法为每个超顶点找到一个最小权重边(一个超顶点是一个连通分量,它只是第一次迭代中的一个顶点)并将它们添加到 MST 中。添加边后,我们更新连通分量并重复查找最小边和合并超顶点过程,直到图中的所有顶点都在一个连通分量中。

由于每个超顶点的 find-min-edge 可以并行完成,我想使用 OpenMP 来执行此操作。这是我想用于并行查找最小值的omp for 循环。

int index[NUM_VERTICES];
#pragma omp parallel private(nthreads, tid, index, min) shared(minedgeindex, setcount, forest, EV, umark)
{
#pragma omp for
  for(int k = 0; k < setcount; k++){  //iterate over supervertices, omp for here

        min = 9999;
        std::fill_n(index, NUM_VERTICES, -1);

    /* Gets minimum edge for each supervertex */
    for(int i = 0; i < NUM_VERTICES; i++) {
         if(forest[i]->mark == umark[k]){    //find vertices with mark k
            for(int j = 0; j < NUM_EDGES; j++) {    
//check min edge for each vertex in the supervertex k
                if(EV[j].v1==i){
                    if(Find(forest[EV[j].v1])!= Find(forest[EV[j].v2])){
                            if(EV[j].w <= min ){
                                    min = EV[j].w;
                                    index[k] = j;
                                    break;  //break looping over edges for current vertex i, go to next vertex i+1
                            }
                    }
                }
            }
         }

    }   //end finding min disjoint-connecting edge for the supervertex with mark k

        if(index[k] != -1){
            minedgeindex.insert(minedgeindex.begin(), index[k]);
        }

    }       //omp for end
}

由于我是 OpenMP 新手,我目前无法使其按预期工作。

让我简要解释一下我在这段代码中所做的事情: setcount 是超顶点的数量。 EV 是一个包含所有边的向量(Edge 是我之前定义的结构,具有属性 v1, v2, w 对应于它连接的两个节点及其权重)。 minedgeindex 是一个向量,我希望每个线程找到每个连通分量的最小边,同时将最小边的索引(EV 中的索引 j)添加到向量 minedgeindex。所以我认为应该分享minedgeindexforest 是每个顶点的结构体,它有一个集合标记umark 指示它在哪个超顶点。我使用Union-Find 标记所有超顶点,但它与这块omp 代码无关。

此代码块的最终目标是为我提供正确的向量minedgeindex,其中包含每个超顶点的所有最小边。

为了更清楚并忽略图形背景,我只有一个很大的数字向量,我将它们分成一堆集合,然后我需要一些并行线程来找到每组数字的最小值并给我回来这些分钟的索引,将它们存储在向量minedgeindex

如果您需要更多说明,请询问我。请帮我完成这项工作,我认为主要问题是私有和共享变量的声明,我不知道我是否做得对。

提前谢谢你!

【问题讨论】:

    标签: c++ openmp


    【解决方案1】:

    在并行块之外分配一个数组然后将其声明为私有是行不通的。

    编辑: 再次阅读您的代码后,index 似乎不应该是私有的。在这种情况下,您应该只在并行块之外声明它(就像您所做的那样),但不要将其声明为私有。但我不确定你甚至需要 index 是一个数组。我认为您可以将其声明为私有 int。

    此外,您不能像以前那样填写minedgeindex。这会导致竞争条件。您需要将其放在关键部分。就我个人而言,我会尝试使用push_back,而不是从数组的开头插入,因为这样效率很低。

    有些人更喜欢明确声明所有内容都是共享的和私有的。在标准 C 中,您必须这样做,至少对于私人而言。但对于 C99/C++,这不是必需的。我宁愿只在必要时声明共享/私有。并行区域之外的所有内容都是共享的(除非它是并行循环中使用的索引),并且内部的所有内容都是私有的。如果您牢记这一点,您很少需要明确声明数据共享或私有。

        //int index[NUM_VERTICES]; //index is shared
        //std::fill_n(index, NUM_VERTICES, -1);
        #pragma omp parallel
        {   
            #pragma omp for
            for(int k = 0; k < setcount; k++){  //iterate over supervertices, omp for here
                int min = 9999; // min is private
                int index = -1;
    
                //iterate over supervertices
    
                if(index != -1){
                    #pragma omp critical
                    minedgeindex.insert(minedgeindex.begin(), index);
                    //minedgeindex.insert(minedgeindex.begin(), index[k]);
                }
            }
        }
    

    现在代码在这里工作了,有一些建议可以加快它的速度。

    在循环内使用critical 声明可能非常低效。我建议填充一个私有数组(std::vector),然后在并行循环之后合并它们(但仍在并行块中)。循环有一个不必要的隐含屏障。这可以使用nowait 删除。

    独立于关键部分,每次迭代找到每个最小值的时间可能会有所不同,因此您可能需要考虑schedule(dynamic)。下面的代码完成了这一切。这些建议的一些变体(如果不是全部)可能会提高您的表现。

    #pragma omp parallel
    {
        vector<int> minedgeindex_private;
        #pragma omp for schedule(dynamic) nowait
        for(int k = 0; k < setcount; k++){  //iterate over supervertices, omp for here
            int min = 9999;
            int index = -1;
    
            //iterate over supervertices
    
            if(index != -1){
                minedgeindex_private.push_back(index);
            }
        }
        #pragma omp critical
        minedgeindex.insert(
            minedgeindex.end(),
            minedgeindex_private.begin(), minedgeindex_private.end());
    }
    

    【讨论】:

    • 非常感谢!它现在完美运行。您的回答非常清楚,非常有帮助。我也更喜欢在不声明私有和共享的情况下按照自己的方式进行操作,这更直观。是的,我应该使用 push_back 而不是 insert!
    • @LoganYang,我很高兴能帮上忙 :-) 表现如何?我添加了一些可能会提高性能的代码。
    • 串行版运行30000个顶点,耗时1524s。带有您的第一个建议的 omp 版本在 912 中运行,我认为这相当不错。我对您的第二个建议有疑问:每个线程都有自己的向量“minedgeindex_private”副本,对吗?如果你看一下我的代码的中间块,在 j 上有一个循环,它找到超顶点 k 的最小值(让我在这里使用 k 作为 k = 1 到 setcount 的循环中的特定数字)。最初,index[k] 存储整个超顶点 k 的最小值。
    • 至于“minedgeindex_private”,它包含该超顶点中每个顶点的所有最小边(因为对于 index[k],存在覆盖值,对于向量,我们将所有值保留为“ push_back") 假设我们有 k 个线程,每个线程对应一个 supervertex,我们现在有 k 'minedgeindex_private's 要合并。他们每个人都有一个边列表,而不是超顶点的一分钟。基本上如果我们使用'minedgeindex_private',让我用它的线程k标记这些私有副本,我们有minedgeindex_private[k]={...}而不是单个索引[k]=j。那么是错的还是我误解了?感谢您的耐心等待!
    • @LoganYang,你说得对,每个线程都有自己的变量minedgeindex_private 的私有副本。但是,您在 k 上并行化循环,因此每个 minedgeindex_private 将由 k 的独立区域作用。当你合并它们时,不应该有重叠。请注意不要将线程数与您正在循环的值混淆。我还没有看到问题,但我没有你的完整代码。您是否测试了我建议的更改?
    【解决方案2】:

    这不会在 openMP 上有效地工作,因为omp for 只是在所有线程之间静态地分割工作,即每个线程都获得了超顶点的公平份额。但是,当tread之间的工作分担不均匀时,每个超顶点的工作可能是不均匀的。

    您可以尝试将dynamicguided 调度与openMP 一起使用,但最好完全避免使用openMP 并使用TBB,而tbb::parallel_for() 可以避免此问题。


    OpenMP 有几个缺点: 1) 它是基于预处理器的 2)它的功能相当有限(这是我上面强调的) 3) 它没有针对 C++(尤其是 C++11)进行标准化

    TBB 是一个完全支持 C++11 的纯 C++ 库(无预处理器 hack)。更多详情请看我对this question的回复

    【讨论】:

    • 您能否解释一下 tbb::parallel_for() 的工作原理以及它比 openMP 更好的地方?
    • @Walter,是的,我想知道为什么tbb:parallel_for() 会比#pragma omp for schedule(dynamic) 更好
    • @redrum 在大多数情况下它可能不会更快,但可以避免背面的所有 openMP 痛苦。 TBB 使用递归的基于任务的并行性,因此循环被递归地划分为块。最终块(未进一步拆分的块)的大小会自动调整,以避免饥饿,即使每次迭代的工作量变化很大。
    • @Walter,知道这很有趣。不过,我不认为 OpenMP 会很痛苦。它很容易使用,我不需要所有的 C++ 细节。在几乎所有情况下,我都找到了一种使用 OpenMP 获得良好效率的方法。默认情况下,MSVC、GCC 和 ICC 也可以使用它,我觉得这非常方便。我不使用clang。但如果 TBB 能更好地处理负载平衡,那么值得考虑。
    猜你喜欢
    • 1970-01-01
    • 2014-02-24
    • 1970-01-01
    • 2011-04-19
    • 1970-01-01
    • 2012-02-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多