如何使用OpenMP并行化通过C ++ std :: list的for循环?

时间:2012-01-01 01:46:50

标签: c++ list parallel-processing openmp

我想使用OpenMP以并行方式遍历std :: list中的所有元素。循环应该能够改变列表的元素。有一个简单的解决方案吗?当迭代器是随机访问迭代器时,似乎OpenMP 3.0支持并行for循环,但不是其他。无论如何,我更喜欢使用OpenMP 2.0,因为我无法完全控制哪些编译器可供我使用。

如果我的容器是矢量,我可能会使用:

#pragma omp parallel for
for (auto it = v.begin(); it != v.end(); ++it) {
    it->process();
}

据我所知,我可以将列表复制到矢量中,执行循环,然后将所有内容复制回来。但是,如果可能的话,我想避免这种复杂性和开销。

5 个答案:

答案 0 :(得分:29)

如果您决定使用Openmp 3.0,则可以使用task功能:

#pragma omp parallel
#pragma omp single
{
  for(auto it = l.begin(); it != l.end(); ++it)
     #pragma omp task firstprivate(it)
       it->process();
  #pragma omp taskwait
}

这将在一个线程中执行循环,但将元素处理委托给其他人。

没有OpenMP 3.0,最简单的方法是编写列表中元素的所有指针(或者向量中的迭代器并迭代那个。这样你就不必复制任何东西了,避免了开销复制元素本身,所以它不应该有太多的开销:

std::vector<my_element*> elements; //my_element is whatever is in list
for(auto it = list.begin(); it != list.end(); ++it)
  elements.push_back(&(*it));

#pragma omp parallel shared(chunks)
{
  #pragma omp for
  for(size_t i = 0; i < elements.size(); ++i) // or use iterators in newer OpenMP
      elements[i]->process();
}

如果你想避免复制指针,你总是可以手工创建一个并行化的for循环。您可以让线程访问列表中的交错元素(由KennyTM提出),也可以在迭代和迭代之前将范围拆分为大致相等的连续部分。后者似乎更可取,因为线程避免访问当前由其他线程处理的列表节点(即使只有下一个指针),这可能导致错误共享。这看起来大致如下:

#pragma omp parallel
{
  int thread_count = omp_get_num_threads();
  int thread_num   = omp_get_thread_num();
  size_t chunk_size= list.size() / thread_count;
  auto begin = list.begin();
  std::advance(begin, thread_num * chunk_size);
  auto end = begin;
  if(thread_num = thread_count - 1) // last thread iterates the remaining sequence
     end = list.end();
  else
     std::advance(end, chunk_size);
  #pragma omp barrier
  for(auto it = begin; it != end; ++it)
    it->process();
}

并不严格需要屏障,但如果process改变已处理的元素(意味着它不是const方法),如果线程迭代序列,那么可能存在某种错误共享。已经变异了。这种方式将在序列上迭代3 * n次(其中n是线程数),因此对于大量线程,缩放可能不是最优的。

为了减少开销,可以将范围的生成放在#pragma omp parallel之外,但是您需要知道将形成并行部分的线程数。因此,您可能必须手动设置num_threads,或使用omp_get_max_threads()并处理创建的线程数小于omp_get_max_threads()(仅为上限)的情况。最后一种方法可以通过在这种情况下分配每个线程的severa块来处理(使用#pragma omp for应该这样做):

int max_threads = omp_get_max_threads();
std::vector<std::pair<std::list<...>::iterator, std::list<...>::iterator> > chunks;
chunks.reserve(max_threads); 
size_t chunk_size= list.size() / max_threads;
auto cur_iter = list.begin();
for(int i = 0; i < max_threads - 1; ++i)
{
   auto last_iter = cur_iter;
   std::advance(cur_iter, chunk_size);
   chunks.push_back(std::make_pair(last_iter, cur_iter);
}
chunks.push_back(cur_iter, list.end();

#pragma omp parallel shared(chunks)
{
  #pragma omp for
  for(int i = 0; i < max_threads; ++i)
    for(auto it = chunks[i].first; it != chunks[i].second; ++it)
      it->process();
}

这将只需要list上的三次迭代(两次,如果你可以获得列表的大小而不进行迭代)。我认为这是关于非随机访问迭代器可以做的最好的事情,而不使用tasks或迭代一些不合适的数据结构(如指针向量)。

答案 1 :(得分:3)

我怀疑这是可能的,因为你不能只是在没有遍历列表的情况下跳到列表的中间。列表不存储在连续的内存中,而std :: list迭代器不是随机访问。它们只是双向的。

答案 2 :(得分:2)

http://openmp.org/forum/viewtopic.php?f=3&t=51

#pragma omp parallel
{
   for(it= list1.begin(); it!= list1.end(); it++)
   {
      #pragma omp single nowait
      {
         it->compute();
      }
   } // end for
} // end ompparallel

这可以理解为展开为:

{
  it = listl.begin
  #pragma omp single nowait
  {
    it->compute();
  }
  it++;
  #pragma omp single nowait
  {
    it->compute();
  }
  it++;
...
}

给出这样的代码:

int main()                                                                      
{                                                                               
        std::vector<int> l(4,0);                                                
        #pragma omp parallel for                                                        
        for(int i=0; i<l.size(); ++i){                                          
                printf("th %d = %d \n",omp_get_thread_num(),l[i]=i);            
        }                                                                       
        printf("\n");                                                           
       #pragma omp parallel                                                            
        {                                                                       
                for (auto i = l.begin(); i != l.end(); ++i) {                   
               #pragma omp single nowait                                                       
                {                                                       
                        printf("th %d = %d \n",omp_get_thread_num(),*i);
                }                                                       
            }                                                               
        }                                                                       
        return 0;                                                               
} 

导出OMP_NUM_THREADS = 4,输出如下(注意第二部分,工作线程编号可以重复):

th 2 = 2 
th 1 = 1 
th 0 = 0 
th 3 = 3 

th 2 = 0 
th 1 = 1 
th 2 = 2 
th 3 = 3

答案 3 :(得分:0)

不使用OpenMP 3.0 ,您可以选择让所有线程遍历列表:

std::list<T>::iterator it;
#pragma omp parallel private(it)
{
   for(it = list1.begin(); it!= list1.end(); it++)
   {
      #pragma omp single nowait
      {
         it->compute();
      }
   } 
} 

在这种情况下,每个线程都有自己的迭代器副本(私有),但只有一个线程将访问特定元素(单个),而其他线程将继续前进到下一个项目( nowait

或者你可以循环一次来构建一个指针向量,然后在线程中分配:

std::vector< T*> items;

items.reserve(list.size());
//put the pointers in the vector
std::transform(list.begin(), list.end(), std::back_inserter(items), 
               [](T& n){ return &n; }
);

#pragma omp parallel for
for (int i = 0; i < items.size(); i++)
{
  items[i]->compute();
}

根据您的具体情况,一个或另一个可以更快。测试哪一个更适合你很容易。

答案 4 :(得分:0)

这是一个允许并行插入/删除列表的新元素的解决方案。

对于包含N元素的列表,我们首先将列表切割为nthreads列表 大约有N/nthreads个元素。在并行区域中,这可以像这样完成

int ithread = omp_get_thread_num();
int nthreads = omp_get_num_threads();
int t0 = (ithread+0)*N/nthreads;
int t1 = (ithread+1)*N/nthreads;

std::list<int> l2;
#pragma omp for ordered schedule(static)
for(int i=0; i<nthreads; i++) {
    #pragma omp ordered
    {
        auto it0 = l.begin(), it1 = it0;
        std::advance(it1, t1-t0);       
        l2.splice(l2.begin(), l2, it0, it1);
    }
}

其中l2是每个帖子的剪切列表。

然后我们可以并行处理每个列表。例如,我们可以在列表中的每个第一个位置插入-1,如此

auto it = l2.begin();
for(int i=(t0+4)/5; i<(t1+4)/5; i++) {
    std::advance(it, 5*i-t0);
    l2.insert(it, -1);
}

最后,在我们对列表并行操作之后,我们将每个线程的列表按顺序拼接回一个列表:

#pragma omp for ordered schedule(static)
for(int i=0; i<nthreads; i++) {
    #pragma omp ordered
    l.splice(l.end(), l, l2.begin(), l2.end());
}

算法本质上是。

  1. 快进列表顺序制作剪切列表。
  2. 并行添加,修改或删除元素的裁剪列表。
  3. 将修改后的剪切列表顺序拼接在一起。
  4. 这是一个工作示例

    #include <algorithm>
    #include <iostream>
    #include <list>
    #include <omp.h>
    
    int main(void) {
      std::list<int> l;
      for(int i=0; i<22; i++) {
        l.push_back(i);
      }
      for (auto it = l.begin(); it != l.end(); ++it) {
        std::cout << *it << " ";
      } std::cout << std::endl;
    
      int N = l.size();
      #pragma omp parallel
      {
        int ithread = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        int t0 = (ithread+0)*N/nthreads;
        int t1 = (ithread+1)*N/nthreads;
    
        //cut list into nthreads lists with size=N/nthreads
        std::list<int> l2;
        #pragma omp for ordered schedule(static)
        for(int i=0; i<nthreads; i++) {
          #pragma omp ordered
          {
        auto it0 = l.begin(), it1 = it0;
        std::advance(it1, t1-t0);       
        l2.splice(l2.begin(), l2, it0, it1);
          }
        }
        //insert -1 every 5th postion
        auto it = l2.begin();
        for(int i=(t0+4)/5; i<(t1+4)/5; i++) {
          std::advance(it, 5*i-t0);
          l2.insert(it, -1);
        }
    
        //splice lists in order back together.
        #pragma omp for ordered schedule(static)
        for(int i=0; i<nthreads; i++) {
          #pragma omp ordered
          l.splice(l.end(), l, l2.begin(), l2.end());
        }  
      }
    
      for (auto it = l.begin(); it != l.end(); ++it) {
        std::cout << *it << " ";
      } std::cout << std::endl;  
    }
    

    结果

    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 
    -1 0 1 2 3 4 -1 5 6 7 8 9 -1 10 11 12 13 14 -1 15 16 17 18 19 -1 20 21