openMp: серьезная потеря производительности при вызове общих ссылок динамических массивов

Я пишу симуляцию cfd и хочу распараллелить мой цикл ~10^5 (размер решетки), который является частью функции-члена. Реализация кода openMp проста: я читаю записи общих массивов, выполняю вычисления с частными потоками и, наконец, снова записываю в общий массив. В каждом массиве я получаю доступ только к элементу массива номера цикла, поэтому не ожидаю условия гонки и не вижу причин для сброса. Тестируя ускорение кода (параллельная часть), я обнаружил, что все, кроме одного процессора, работают всего на ~70%. У кого-нибудь есть идеи, как это улучшить?

void class::funcPar(bool parallel){
#pragma omp parallel
{
    int one, two, three;
    double four, five;

    #pragma omp for
    for(int b=0; b<lenAr; b++){
        one = A[b]+B[b];
        C[b] = one;
        one += D[b];
        E[b] = one;
    }
}

}

1 ответ

Решение

Несколько пунктов, затем тестовый код, затем обсуждение:

  1. 10^5 не так много, если каждый предмет int, Затраты на запуск нескольких потоков могут быть больше, чем выгоды.
  2. Оптимизация компилятора может быть испорчена при использовании OMP.
  3. При работе с несколькими операциями на набор памяти циклы могут быть связаны с памятью (т. Е. Процессор тратит время на ожидание доставки запрошенной памяти)

Как и было обещано, вот код:

#include <iostream>
#include <chrono>
#include <Eigen/Core>


Eigen::VectorXi A;
Eigen::VectorXi B;
Eigen::VectorXi D;
Eigen::VectorXi C;
Eigen::VectorXi E;
int size;

void regular()
{
    //#pragma omp parallel
    {
        int one;
//      #pragma omp for
        for(int b=0; b<size; b++){
            one = A[b]+B[b];
            C[b] = one;
            one += D[b];
            E[b] = one;
        }
    }
}

void parallel()
{
#pragma omp parallel
    {
        int one;
        #pragma omp for
        for(int b=0; b<size; b++){
            one = A[b]+B[b];
            C[b] = one;
            one += D[b];
            E[b] = one;
        }
    }
}

void vectorized()
{
    C = A+B;
    E = C+D;
}

void both()
{
    #pragma omp parallel
    {
        int tid = omp_get_thread_num();
        int nthreads = omp_get_num_threads();
        int vals = size / nthreads;
        int startInd = tid * vals;
        if(tid == nthreads - 1)
            vals += size - nthreads * vals;
        auto am = Eigen::Map<Eigen::VectorXi>(A.data() + startInd, vals);
        auto bm = Eigen::Map<Eigen::VectorXi>(B.data() + startInd, vals);
        auto cm = Eigen::Map<Eigen::VectorXi>(C.data() + startInd, vals);
        auto dm = Eigen::Map<Eigen::VectorXi>(D.data() + startInd, vals);
        auto em = Eigen::Map<Eigen::VectorXi>(E.data() + startInd, vals);
        cm = am+bm;
        em = cm+dm;
    }
}
int main(int argc, char* argv[])
{
    srand(time(NULL));
    size = 100000;
    int iterations = 10;
    if(argc > 1)
        size = atoi(argv[1]);
    if(argc > 2)
        iterations = atoi(argv[2]);
    std::cout << "Size: " << size << "\n";
    A = Eigen::VectorXi::Random(size);
    B = Eigen::VectorXi::Random(size);
    D = Eigen::VectorXi::Random(size);
    C = Eigen::VectorXi::Zero(size);
    E = Eigen::VectorXi::Zero(size);

    auto startReg = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        regular();
    auto endReg = std::chrono::high_resolution_clock::now();

    std::cerr << C.sum() - E.sum() << "\n";

    auto startPar = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        parallel();
    auto endPar = std::chrono::high_resolution_clock::now();

    std::cerr << C.sum() - E.sum() << "\n";

    auto startVec = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        vectorized();
    auto endVec = std::chrono::high_resolution_clock::now();

    std::cerr << C.sum() - E.sum() << "\n";

    auto startPVc = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; i++)
        both();
    auto endPVc = std::chrono::high_resolution_clock::now();

    std::cerr << C.sum() - E.sum() << "\n";

    std::cout << "Timings:\n";
    std::cout << "Regular:    " << std::chrono::duration_cast<std::chrono::microseconds>(endReg - startReg).count() / iterations << "\n";
    std::cout << "Parallel:   " << std::chrono::duration_cast<std::chrono::microseconds>(endPar - startPar).count() / iterations << "\n";
    std::cout << "Vectorized: " << std::chrono::duration_cast<std::chrono::microseconds>(endVec - startVec).count() / iterations << "\n";
    std::cout << "Both      : " << std::chrono::duration_cast<std::chrono::microseconds>(endPVc - startPVc).count() / iterations << "\n";

    return 0;
}

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

g ++ -fopenmp -std = C++ 11 -Wall -pedantic -pthread -IC: \ usr \ include source.cpp -o a.exe

g ++ -fopenmp -std = C++ 11 -Wall -pedantic -pthread -O1 -IC: \ usr \ include source.cpp -o aO1.exe

g ++ -fopenmp -std = C++ 11 -Wall -pedantic -pthread -O2 -IC: \ usr \ include source.cpp -o aO2.exe

g ++ -fopenmp -std = C++11 -Wall -pedantic -pthread -O3 -I C:\usr\include source.cpp -o aO3.exe

используя g++ (x86_64-posix-sjlj, созданный проектом strawberryperl.com) 4.8.3 под Windows.

обсуждение

Начнем с рассмотрения 10^5 против 10^6 элементов, усредненных в 100 раз без оптимизации.

10^5 (без оптимизации):

Timings:
Regular:    9300
Parallel:   2620
Vectorized: 2170
Both      : 910

10^6 (без оптимизации):

Timings:
Regular:    93535
Parallel:   27191
Vectorized: 21831
Both      : 8600

Векторизация (SIMD) превосходит OMP с точки зрения ускорения. Вместе мы получаем еще лучшие времена.

Переезд в -O1:

10^5:

Timings:
Regular:    780
Parallel:   300
Vectorized: 80
Both      : 80

10^6:

Timings:
Regular:    7340
Parallel:   2220
Vectorized: 1830
Both      : 1670

То же, что и без оптимизаций, за исключением того, что время гораздо лучше.

Пропуск вперед до -O3:

10^5:

Timings:
Regular:    380
Parallel:   130
Vectorized: 80
Both      : 70

10^6:

Timings:
Regular:    3080
Parallel:   1750
Vectorized: 1810
Both      : 1680

Для 10^5 оптимизация по-прежнему козырная. Однако 10^6 дает более быстрое время для циклов OMP, чем векторизация.

Во всех тестах мы получили ускорение x2-x4 для OMP.

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

Заключение

Ваш пример минимального кода не работает так, как заявлено. Такие проблемы, как шаблоны доступа к памяти, могут возникать с более сложными данными. Добавьте достаточно деталей, чтобы точно воспроизвести вашу проблему ( MCVE), чтобы получить лучшую помощь.

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