Как распараллелить цикл for через C ++ std :: list с использованием OpenMP? - PullRequest
20 голосов
/ 01 января 2012

Я бы хотел пройти через все элементы в std :: list параллельно, используя OpenMP.Цикл должен быть в состоянии изменить элементы списка.Есть ли простое решение для этого?Кажется, что OpenMP 3.0 поддерживает параллельные циклы for, когда итератор является итератором произвольного доступа, но не иначе.В любом случае, я бы предпочел использовать OpenMP 2.0, поскольку у меня нет полного контроля над тем, какие компиляторы мне доступны.

Если бы мой контейнер был вектором, я мог бы использовать:

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

Я понимаю, что могу скопировать список в вектор, сделать цикл, а затем скопировать все обратно.Однако я хотел бы избежать этой сложности и накладных расходов, если это возможно.

Ответы [ 5 ]

30 голосов
/ 03 января 2012

Если вы решите использовать 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() (что является только верхней границей).Последний способ может быть обработан, возможно, назначив каждому потоку несколько кусков в этом случае (с помощью #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 или итерируя по какой-то неестественной структуре данных (например, по указателю).

3 голосов
/ 01 января 2012

Я сомневаюсь, что это возможно, поскольку вы не можете просто перейти в середину списка, не пройдя по списку. Списки не хранятся в непрерывной памяти, а итераторы std :: list не являются произвольным доступом. Они только двунаправленные.

2 голосов
/ 25 февраля 2014

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
0 голосов
/ 05 октября 2017

Вот решение, которое позволяет параллельно вставлять / удалять новые элементы списка.

Для списка с 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. Сращивание измененных списков вырезов обратно последовательно.

Вот рабочий пример

#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
0 голосов
/ 24 августа 2017

Без использования 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();
      }
   } 
} 

В этом случае каждый поток имеет свою собственную копию итератора ( private ), но только один поток будет обращаться к определенному элементу ( single ), тогда как другие потоки будут двигаться вперед к следующим предметам ( 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();
}

В зависимости от вашего конкретного случая один или другой может быть быстрее. Проверить, какой из них подходит вам лучше, просто.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...