Я новичок в openMP. Я работаю над алгоритмом кратчайшего пути для всех пар, и вот последовательная процедура C++, которую мне нужно распараллелить (полный код в конце поста):
void mini(vector<vector<double>> &M, size_t n, vector<double> &rowk, vector<double> &colk)
{
size_t i, j;
for ( i=0; i<n; i++)
for ( j=0; j<n; j++)
M[i][j]=min(rowk[j]+colk[i], M[i][j]);
}
При выполнении получаю следующее:
$ time ./floyd
real 0m0,349s
user 0m0,349s
sys 0m0,000s
Теперь я пытаюсь вставить некоторые директивы:
void mini(vector<vector<double>> &M, size_t n, vector<double> &rowk, vector<double> &colk)
{
#pragma omp parallel
{
size_t i, j;
#pragma omp parallel for
for ( i=0; i<n; i++)
for ( j=0; j<n; j++)
M[i][j]=min(rowk[j]+colk[i], M[i][j]);
}
}
К сожалению, ускорения нет:
$ grep -c ^processor /proc/cpuinfo
4
$ time ./floyd
real 0m0,547s
user 0m2,073s
sys 0m0,004s
Что я делаю неправильно?
Процессор: Intel(R) Core(TM) i5-4590 CPU (4 аппаратных ядра)
Полный код:
#include <cstdio>
#include <vector>
#include <limits>
#include <ctime>
#include <random>
#include <set>
#include <omp.h>
using namespace std;
typedef struct Wedge
{
int a, b;
double w;
} Wedge;
typedef pair<int, int> edge;
int randrange(int end, int start=0)
{
random_device rd;
mt19937 gen(rd());
uniform_int_distribution<> dis(start, end-1);
return dis(gen);
}
void relax_omp(vector<vector<double>> &M, size_t n, vector<double> &rowk, vector<double> &colk)
{
#pragma omp parallel
{
size_t i, j;
#pragma omp parallel for
for (i=0; i<n; i++)
for ( j=0; j<n; j++)
M[i][j]=min(rowk[j]+colk[i], M[i][j]);
}
}
void relax_serial(vector<vector<double>> &M, size_t n, vector<double> &rowk, vector<double> &colk)
{
size_t i, j;
for (i=0; i<n; i++)
for ( j=0; j<n; j++)
M[i][j]=min(rowk[j]+colk[i], M[i][j]);
}
void floyd(vector<vector<double>> &dist, bool serial)
{
size_t i, k;
size_t n {dist.size()};
for (k=0; k<n; k++)
{
vector<double> rowk =dist[k];
vector<double> colk(n);
for (i=0; i<n; i++)
colk[i]=dist[i][k];
if (serial)
relax_serial(dist, n, rowk, colk);
else
relax_omp(dist, n, rowk, colk);
}
for (i=0; i<n; i++)
dist[i][i]=0;
}
vector<Wedge> random_edges(int n, double density, double max_weight)
{
int M{n*(n-1)/2};
double m{density*M};
set<edge> edges;
vector<Wedge> wedges;
while (edges.size()<m)
{
pair<int,int> L;
L.first=randrange(n);
L.second=randrange(n);
if (L.first!=L.second && edges.find(L) == edges.end())
{
double w=randrange(max_weight);
Wedge wedge{L.first, L.second, w};
wedges.push_back(wedge);
edges.insert(L);
}
}
return wedges;
}
vector<vector<double>> fill_distances(vector<Wedge> wedges, int n)
{
double INF = std::numeric_limits<double>::infinity();
size_t i, m=wedges.size();
vector<vector<double>> dist(n, vector<double>(n, INF));
int a, b;
double w;
for (i=0; i<m; i++)
{ a=wedges[i].a;
b=wedges[i].b;
w=wedges[i].w;
dist[a][b]=w;
}
return dist;
}
int main (void)
{
double density{0.33};
double max_weight{200};
int n{800};
bool serial;
int ntest=10;
double avge_serial=0, avge_omp=0;
for (int i=0; i<ntest; i++)
{
vector<Wedge> wedges=random_edges(n, density, max_weight);
vector<vector<double>> dist=fill_distances(wedges, n);
double dtime;
dtime = omp_get_wtime();
serial=true;
floyd(dist, serial);
dtime = omp_get_wtime() - dtime;
avge_serial+=dtime;
dtime = omp_get_wtime();
serial=false;
floyd(dist, serial);
dtime = omp_get_wtime() - dtime;
avge_omp+=dtime;
}
printf("%d tests, n=%d\n", ntest, n);
printf("Average serial : %.2lf\n", avge_serial/ntest);
printf("Average openMP : %.2lf\n", avge_omp/ntest);
return 0;
}
выход :
20 tests, n=800
Average serial : 0.31
Average openMP : 0.61
командная строка:
g++ -std=c++11 -Wall -O2 -Wno-unused-result -Wno-unused-variable -Wno-unused-but-set-variable -Wno-unused-parameter floyd.cpp -o floyd -lm -fopenmp
Не используйте time
для оценки выступлений, для этого есть более тонкие способы. Например, используя clock_gettime() с монотонными часами. И убедитесь, что ваш код скомпилирован как минимум с ключом -O2.
Я скомпилировал с флагом оптимизации O2
Пробовали ли вы большее значение n
? 800 вроде не много.
хм... это не совсем то, что я имел в виду под повторением 20 раз, потому что это все еще запускает и останавливает работу 20 раз, добавляя накладные расходы на переключение потоков. Я хотел расширить работу, сохраняя темы живыми, я добавлю еще несколько предложений.
@Трэвис. Я сделал, но сериал всегда выигрывал.
Этому может быть много причин, наиболее очевидной из которых является то, что рабочая нагрузка слишком мала, чтобы заметить ускорение. Начальная рабочая нагрузка составляет 300 мс. Я бы предложил включить это в последовательный внешний цикл, который повторяет эту работу не менее 20 раз, тогда вы начинаете с последовательного времени (300 мс * 20) 6 секунд для тестирования.
Другим фактором является наличие параллельных ядер на машине, на которой вы работаете. Если ваш процессор имеет одно ядро, многопоточность вызовет замедление из-за стоимости переключения потоков. 2 логических ядра должны показать некоторое ускорение, 2 физических ядра могут показать близкое к линейному ускорение.
Использование только директив pragma также не гарантирует, что используется openMP. Вы должны скомпилировать с использованием аргумента командной строки -fopenmp
, чтобы гарантировать, что библиотека openmp связана с вашим объектным кодом.
Редактировать
Глядя на ваш код сейчас, фактор, который контролирует объем работы, кажется, N, а не внешний цикл. Идея внешнего цикла заключалась в том, чтобы искусственно увеличить объем работы за тот же период времени, но здесь этого сделать нельзя, поскольку вы пытаетесь решить конкретную проблему. Вы также можете попробовать распараллелить вложенный цикл, но я думаю, что N = 800 слишком мало для распараллеливания, чтобы что-то изменить.
#pragma omp parallel for private(j) collapse(2)
j должен быть закрытым для каждой итерации внешнего цикла, следовательно, private(j), в противном случае j будет совместно использоваться всеми потоками, что приведет к неточному результату.
Ваш цикл выполняется 640 000 раз, что немного для современных процессоров с тактовой частотой 3 ГГц+, попробуйте что-нибудь около N = 5000, что составляет 25 миллионов итераций.
Алгоритм All-Pairs Shortest Path имеет ту же сложность, что и умножение матриц, и алгоритмы очень похожи. И, к моему удивлению, на моей машине код умножения матриц omp дал мне ускорение в 4 раза. Кстати, я действительно связался с fopenmp.
Спасибо, Авин, за то, что следил за моим редактированием. Я попробовал ваше предложение (n = 5000), но последовательный снова выиграл (111,41 с против 127,72 с). Теперь я рассматриваю возможность использования потоков и ручного распараллеливания внешнего цикла.
хм... это довольно удивительно. Это определенно должно работать при таком размере выборки. Следили ли вы за использованием процессора во время параллельного запуска? С С++ он должен достигать 100% на всех ядрах.
@Alvin: да, все ядра заняты.
Ваша основная проблема заключается в том, что вы случайно используете вложенный параллелизм:
#pragma omp parallel
{
size_t i, j;
#pragma omp parallel for
Поскольку вы уже находитесь в параллельном регионе, ваша вторая линия должна быть
#pragma omp for
В противном случае, поскольку omp parallel for
равно omp parallel
и omp for
, у вас есть две вложенные параллельные области, что обычно плохо. Исправление этой незначительной проблемы дает примерно 2-кратное ускорение на аналогичном процессоре.
Есть несколько ограничений, из-за которых вы вряд ли получите полное 4-кратное ускорение, например:
Редактировать: Между прочим, гораздо более идиоматический способ написания кода следующий:
void relax_omp(...) {
#pragma omp parallel for
for (size_t i=0; i<n; i++) {
for (size_t j=0; j<n; j++) {
M[i][j]=min(rowk[j]+colk[i], M[i][j]);
}
}
}
Если вы объявляете переменные как можно более локально, OpenMP почти всегда будет действовать правильно. Что в данном случае означает, что i
и j
являются частными. В общем, так гораздо проще рассуждать о коде.
Спасибо, Зулан, ваш ответ — большой прогресс: для n = 800 время выполнения делится в 3,3 раза, а для n = 1000 — в 1,94 раза.
Распараллеливание алгоритма/приложения не гарантирует его ускорение. Во многих случаях это не будет иметь никакого значения, а в некоторых действительно замедлит его из-за накладных расходов на синхронизацию и других вещей. Это не должно удивлять.