Параллельные задачи улучшают производительность с boost:: thread, чем с ppl или OpenMP

У меня есть программа на С++, которая может быть распараллелена. Я использую компиляцию Visual Studio 2010, 32bit.

Короче, структура программы следующая

#define num_iterations 64 //some number

struct result
{ 
    //some stuff
}

result best_result=initial_bad_result;

for(i=0; i<many_times; i++)
{ 
    result *results[num_iterations];


    for(j=0; j<num_iterations; j++)
    {
        some_computations(results+j);
    }

    // update best_result; 
}

Поскольку каждый some_computations() является независимым (некоторые глобальные переменные считаются, но не изменяются глобальные переменные), я распараллеливал внутренний for -loop.

Моя первая попытка заключалась в boost:: thread,

 thread_group group;
 for(j=0; j<num_iterations; j++)
 {
     group.create_thread(boost::bind(&some_computation, this, result+j));
 } 
 group.join_all();

Результаты были хорошими, но я решил попробовать больше.

Я попробовал библиотеку OpenMP

 #pragma omp parallel for
 for(j=0; j<num_iterations; j++)
 {
     some_computations(results+j);
 } 

Результаты были хуже, чем boost::thread.

Затем я попробовал библиотеку ppl и использовал parallel_for():

 Concurrency::parallel_for(0,num_iterations, [=](int j) { 
     some_computations(results+j);
 })

Результаты были самыми плохими.

Я нашел это поведение совершенно неожиданным. Поскольку OpenMP и ppl предназначены для распараллеливания, я ожидал бы лучших результатов, чем boost::thread. Я не прав?

Почему boost::thread дает мне лучшие результаты?

Ответ 1

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

Не видя, как вы реализовали эти вещи, трудно сказать, какова может быть настоящая причина.

Также, если операции в каждой итерации имеют некоторую зависимость от любых других итераций в одном цикле, тогда это создаст конкуренцию, что замедлит работу. Вы не показали, что делает ваша функция some_operation, поэтому трудно определить, существуют ли зависимости данных.

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

Не все петли могут быть распараллелены, это очень зависит от типа выполняемой работы.

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

Кроме того, если у вас есть непрерывный массив, этот массив может быть частично в строке кэша, и если вы редактируете элемент 5 в потоке A, а затем меняете элемент 6 в потоке B, вы можете получить утверждение о кеше, которое также будет замедлить работу, поскольку они будут находиться в одной и той же строке кэша. Явление, известное как ложное разделение.

Существует много аспектов, которые следует учитывать при выполнении параллелизации цикла.

Ответ 2

Короче говоря, openMP в основном базируется на общей памяти, с дополнительными затратами на управление задачами и управление памятью. ppl предназначен для обработки общих шаблонов общих структур данных и алгоритмов, что приносит дополнительную сложность. У обоих из них есть дополнительная стоимость процессора, но ваши простые потоки boost не имеют потоков (boost - это просто простая упаковка API). Вот почему они оба медленнее, чем ваша версия boost. И поскольку расчетные вычисления независимы друг от друга, без синхронизации, openMP должен быть близок к версии boost.

Это происходит в простых сценариях, но для сложных сценариев со сложной компоновкой данных и алгоритмами он должен быть зависимым от контекста.