Две последовательные параллельные области OpenMP замедляют друг друга - PullRequest
0 голосов
/ 15 октября 2018

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

Часовое тестированиепозже я заметил, что omp parallel for во втором регионе вызывает такое поведение.Кажется, что две последовательные параллельные области замедляют друг друга.Я написал небольшую тестовую программу, чтобы проверить это:

#include <iostream>
#include <chrono>
#include <thread>
#include <omp.h>
#include <vector>
#include <cmath>

using namespace std::chrono_literals;

inline void func1()
{
    // simulate a cpu function that takes 5ms
    std::this_thread::sleep_for(5ms);
}

inline void func2()
{
    // simulate a cpu function that takes 6ms
    std::this_thread::sleep_for(6ms);
}

int main()
{
    // initialize some vectors to test an omp parallel for pragma
    std::vector<float> vec1(10000);
    std::vector<float> vec2(10000);
    std::vector<float> vec3(10000);
    for(int i = 0; i < 10000; i++)
    {
        vec1[i] = (i+1)/1000.0;
        vec2[i] = i;
        vec3[i] = 0;
    }

    // timings taken via std::chrono
    typedef std::chrono::time_point<std::chrono::high_resolution_clock>
                                          time_point;
    typedef std::chrono::duration<double, std::milli> duration;

    // first block
    std::cout << "serial wait, serial loop" << std::endl;
    for(int k = 0; k < 20; k++)
    {

        time_point start = std::chrono::high_resolution_clock::now();

        func1();
        func2();

        duration time1 = std::chrono::high_resolution_clock::now() - start;
        start = std::chrono::high_resolution_clock::now();

        for(int i = 0; i < 10000; i++)
        {
            vec3[i] = sqrt(sin(pow(vec1[i],vec2[i]))*sin(0.5*pow(vec1[i],vec2[i])));
        }

        duration time2 = std::chrono::high_resolution_clock::now() - start;

        std::cout << k << " " << time1.count() << " " << time2.count() << std::endl;
    }

    // second block
    std::cout << "parallel wait, serial loop" << std::endl;
    for(int k = 0; k < 20; k++)
    {

        time_point start = std::chrono::high_resolution_clock::now();

        #pragma omp parallel num_threads(2)
        {
            if(omp_get_thread_num() == 0)
            {
                func1();
            }
            else
            {
                func2();
            }
        }

        duration time1 = std::chrono::high_resolution_clock::now() - start;
        start = std::chrono::high_resolution_clock::now();

        for(int i = 0; i < 10000; i++)
        {
            vec3[i] = sqrt(sin(pow(vec1[i],vec2[i]))*sin(0.5*pow(vec1[i],vec2[i])));
        }

        duration time2 = std::chrono::high_resolution_clock::now() - start;

        std::cout << k << " " << time1.count() << " " << time2.count() << std::endl;
    }

    // third block
    std::cout << "serial wait, parallel loop" << std::endl;
    for(int k = 0; k < 20; k++)
    {

        time_point start = std::chrono::high_resolution_clock::now();

        func1();
        func2();

        duration time1 = std::chrono::high_resolution_clock::now() - start;
        start = std::chrono::high_resolution_clock::now();

        #pragma omp parallel for
        for(int i = 0; i < 10000; i++)
        {
            vec3[i] = sqrt(sin(pow(vec1[i],vec2[i]))*sin(0.5*pow(vec1[i],vec2[i])));
        }

        duration time2 = std::chrono::high_resolution_clock::now() - start;

        std::cout << k << " " << time1.count() << " " << time2.count() << std::endl;
    }

    // fourth block <-- weird behavior
    std::cout << "parallel wait, parallel loop" << std::endl;
    for(int k = 0; k < 20; k++)
    {

        time_point start = std::chrono::high_resolution_clock::now();

        #pragma omp parallel num_threads(2)
        {
            if(omp_get_thread_num() == 0)
            {
                func1();
            }
            else
            {
                func2();
            }
        }

        duration time1 = std::chrono::high_resolution_clock::now() - start;
        start = std::chrono::high_resolution_clock::now();

        #pragma omp parallel for
        for(int i = 0; i < 10000; i++)
        {
            vec3[i] = sqrt(sin(pow(vec1[i],vec2[i]))*sin(0.5*pow(vec1[i],vec2[i])));
        }

        duration time2 = std::chrono::high_resolution_clock::now() - start;

        std::cout << k << " " << time1.count() << " " << time2.count() << std::endl;
    }    
}

Если я запускаю это, я получаю из консоли:

serial wait, serial loop
0 11.8541 3.23881
1 11.4908 3.18409
2 11.8729 3.12847
3 11.6656 3.19606
4 11.8484 3.14534
5 11.863 3.20833
6 11.8331 3.13007
7 11.8351 3.20697
8 11.8337 3.14418
9 11.8361 3.21004
10 11.833 3.12995
11 11.8349 3.14703
12 11.8341 3.1457
13 11.8324 3.14509
14 11.8339 3.12721
15 11.8382 3.14233
16 11.8368 3.14509
17 11.8335 3.14625
18 11.832 3.15115
19 11.8341 3.14499
parallel wait, serial loop
0 6.59906 3.14325
1 6.42459 3.14945
2 6.42381 3.13722
3 6.43271 3.19783
4 6.42408 3.12781
5 6.42404 3.14482
6 6.42534 3.20757
7 6.42392 3.14144
8 6.425 3.14805
9 6.42331 3.1312
10 6.4228 3.14783
11 6.42556 3.15106
12 6.42523 3.14562
13 6.42523 3.14605
14 6.42399 3.12967
15 6.42273 3.14699
16 6.42276 3.15026
17 6.42471 3.14164
18 6.42302 3.14701
19 6.42483 3.19149
serial wait, parallel loop
0 11.8319 4.51681
1 11.4756 0.928738
2 11.1129 0.221045
3 11.1075 0.220827
4 11.1081 0.220197
5 11.1065 0.218774
6 11.1059 0.218329
7 11.1658 0.218804
8 11.1063 0.218056
9 11.107 0.21789
10 11.108 0.218605
11 11.1059 0.217867
12 11.1218 0.218198
13 11.1059 0.217666
14 11.1056 0.219443
15 11.1064 0.217653
16 11.106 0.21729
17 11.1064 0.217565
18 11.1085 0.217965
19 11.1056 0.21735
parallel wait, parallel loop
0 6.41053 6.92563
1 6.06954 4.88433
2 6.4147 0.948097
3 6.41245 5.95226
4 6.41169 4.20988
5 6.41415 3.34145
6 6.41655 4.26902
7 6.41321 1.80355
8 6.41332 1.53747
9 6.41386 1.5394
10 6.06738 1.88866
11 6.41286 1.531
12 6.4133 1.53643
13 6.41356 6.40577
14 6.70144 3.48257
15 6.41551 3.60291
16 6.39516 4.44704
17 6.92893 0.981749
18 6.41533 1.50914
19 6.41685 8.36792

Первые три блока вывода, как и следовало ожидать:для последовательного ожидания в течение 5 и 6 мс требуется около 11 мс.Расчет вектора 3.1мс.Если я распараллеливаю два ожидания, это занимает столько же времени, сколько и самое медленное из обоих (6 мс).И (12-ниточный) параллельный цикл for занимает около 0,22 мс.

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

Может кто-нибудь объяснить мне, почему это происходит и как это решить?

Я компилирую, используя:

g++ main.cpp -O3 -fompenmp

Я также протестировал этот код на ПК с Windows и увидел то же поведение.

Если я удаляю num_threads (2) вВ первом параллельном регионе проблема менее серьезна, но все еще очевидна.(Но num_threads (2) должен затрагивать только первый регион, не так ли?)

Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 12 ноября 2018

Как указал Зулан, «решение» поддерживает параллельные области живыми:

// fifth block <-- "solution"
std::cout << "parallel wait, parallel loop" << std::endl;
for(int k = 0; k < 20; k++)
{

    time_point start = std::chrono::high_resolution_clock::now();

    #pragma omp parallel
    {
        if(omp_get_thread_num() == 0)
        {
            func1();
        }
        else if(omp_get_thread_num() == 1)
        {
            func2();
        }

        #pragma omp for
        for(int i = 0; i < 10000; i++)
        {
            vec3[i] = sqrt(sin(pow(vec1[i],vec2[i]))*sin(0.5*pow(vec1[i],vec2[i])));
        }
    }
    duration time1 = std::chrono::high_resolution_clock::now() - start;

    std::cout << k << " " << time1.count() << std::endl;
} 

Таким образом, результат даже быстрее, чем сумма параллельного ожидания (2-й блок) исинхронизация параллельного цикла (3-й блок) (~ 6,3 мс) в каждой итерации.К сожалению, это решение не работает в моем реальном приложении, но я начну другую тему для этой проблемы.

Я заметил, что проблема возникает только при использовании гиперпоточности.Мой процессор имеет 6 ядер, поддерживающих 12 потоков с использованием гиперпоточности.Если я запускаю тестовый код с OMP_NUM_THREADS = 6, странное поведение в 4-м блоке исчезает.

0 голосов
/ 15 октября 2018

У меня есть некоторые проблемы с постоянным воспроизведением этого, но я верю, что это именно то, что происходит.

Libgomp (среда выполнения OpenMP для gcc) использует пул потоков .Если у вас просто есть последовательные циклы parallel, новые потоки не создаются.Однако, если вы чередуете parallel for - который использует 12 потоков, а parallel num_threads(2), libgomp решает сократить пул потоков до 2 и снова увеличить его до 12.

Я подтвердил это, напечатав gettid() потока OpenMP # 1 / # 2 в параллельном цикле.В то время как # 1 сохраняет свой pid, # 2 получает новый для каждой итерации.Как вы заметили, вы можете легко это исправить.Как отметил Шон, в любом случае parallel sections является более идиоматическим решением.

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

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