От серийного к omp: без ускорения

Я новичок в 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

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

Jesper Juhl 30.05.2019 19:28

Не используйте time для оценки выступлений, для этого есть более тонкие способы. Например, используя clock_gettime() с монотонными часами. И убедитесь, что ваш код скомпилирован как минимум с ключом -O2.

Alain Merigot 30.05.2019 20:28

Я скомпилировал с флагом оптимизации O2

elasstiika 30.05.2019 21:04

Пробовали ли вы большее значение n? 800 вроде не много.

Travis Gockel 31.05.2019 02:39

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

Avin Kavish 31.05.2019 04:28

@Трэвис. Я сделал, но сериал всегда выигрывал.

elasstiika 31.05.2019 11:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
115
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Этому может быть много причин, наиболее очевидной из которых является то, что рабочая нагрузка слишком мала, чтобы заметить ускорение. Начальная рабочая нагрузка составляет 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.

elasstiika 30.05.2019 21:13

Спасибо, Авин, за то, что следил за моим редактированием. Я попробовал ваше предложение (n = 5000), но последовательный снова выиграл (111,41 с против 127,72 с). Теперь я рассматриваю возможность использования потоков и ручного распараллеливания внешнего цикла.

elasstiika 31.05.2019 11:09

хм... это довольно удивительно. Это определенно должно работать при таком размере выборки. Следили ли вы за использованием процессора во время параллельного запуска? С С++ он должен достигать 100% на всех ядрах.

Avin Kavish 31.05.2019 12:35

@Alvin: да, все ядра заняты.

elasstiika 31.05.2019 12:57
Ответ принят как подходящий

Ваша основная проблема заключается в том, что вы случайно используете вложенный параллелизм:

#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 раза.

elasstiika 31.05.2019 14:17

Другие вопросы по теме