簡體   English   中英

如何使用OpenMP並行化通過C ++ std :: list的for循環?

[英]How do I parallelize a for loop through a C++ std::list using 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();
}

我知道我可以將列表復制到矢量中,執行循環,然后將所有內容復制回來。 但是,如果可能的話,我想避免這種復雜性和開銷。

如果您決定使用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之外的范圍生成#pragma omp parallel ,但是您需要知道將形成並行部分的線程數。 因此,您可能必須手動設置num_threads ,或使用omp_get_max_threads()並處理創建的線程數小於omp_get_max_threads() (僅為上限)的情況。 最后一種方法可以通過在這種情況下分配每個線程severa塊來處理(使用#pragma omp for should):

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或迭代一些不合適的數據結構(如指針向量)。

我懷疑這是可能的,因為你不能只是跳到列表的中間而不遍歷列表。 列表不存儲在連續的內存中,而std :: list迭代器不是隨機訪問。 它們只是雙向的。

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;                                                               
} 

export 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

在不使用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();
}

根據您的具體情況,一個或另一個可以更快。 測試哪一個更適合你很容易。

這是一個允許並行插入/刪除列表的新元素的解決方案。

對於具有N元素的列表,我們首先將列表切割為具有大致N/nthreads元素的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. 將修改后的切割列表順序拼接在一起。

這是一個有效的例子

#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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM