OpenMP медленнее, чем ожидалось на MSVC, используя упорядоченную прагму - PullRequest
0 голосов
/ 10 мая 2019

Я пытаюсь ускорить мой png-кодер, используя openmp (omp order pragma), который прекрасно работает с GCC (MinGW).На MSVC результирующее ускорение сильно варьируется.Размер проблемы, над которой я работаю, означает, что распараллеленный цикл for состоит примерно из шести (6) итераций.Запустив это на машине 4C / 8T, я ожидаю, что будет выполнено примерно две «волны» потоков.Это должно занять примерно в два раза больше времени, чем одна итерация на одном ядре.Опять же, это примерно то, что я вижу с GCC, но не с MSVC (занимает примерно 3-4 раза дольше).

Мне удалось отыскать простой пример, демонстрирующий то же поведение.Играя с некоторыми параметрами (количество итераций, время вычислений и т. Д.), Я обнаружил, что MSVC работает в основном противоречиво.Добавление нескольких итераций немного помогает в согласованности, что имеет смысл, но это не то, что я могу сделать в исходной задаче.

#include <windows.h>
#include <stdint.h>
#include <stdio.h>

// Timer stuff...
int64_t get_ts() {
    LARGE_INTEGER li;
    QueryPerformanceCounter(&li);
    return li.QuadPart;
}
double get_time_ms(int64_t prev) {
    LARGE_INTEGER li;
    QueryPerformanceFrequency(&li);
    double frequency = (double)li.QuadPart;
    QueryPerformanceCounter(&li);
    return (double)(li.QuadPart-prev)/(frequency*1E-3);
}
#define TIMING_START int64_t _start = get_ts()
#define TIMING_END printf("In %s: %.02fms\n", __FUNCTION__, get_time_ms(_start));



// Takes roughly 1.7ms on an old i7
void mySlowFunc() {
    //TIMING_START;
    volatile int a = 0;
    for(int j = 0; j < 1000001; j++) {
        a += j;
    }
    //TIMING_END;
}

#define NUM_ITER 6
int main(int argc, char* argv[]) {

    // baseline for comparison
    printf("===== Call on single core:\n");
    for(int i = 0; i < 5; i++) {
        TIMING_START;
        for(int i = 0; i < NUM_ITER; i++) {
            mySlowFunc();
        }
        TIMING_END;
    }

    printf("===== Call on multiple cores: %d\n", omp_get_max_threads());
    for(int i = 0; i < 5; i++) {
        TIMING_START;

#pragma omp parallel
{
        int y;
#pragma omp for ordered
        for(y = 0; y < NUM_ITER; y++) {
            mySlowFunc();

#pragma omp ordered
            {
                volatile int i = 0;
            }
        }
}
        TIMING_END;
    }

    return 0;
}

Следующие примеры выходных данных находятся на той же машине, в той же ОС (Win10, Intel i7860)

Пример вывода MinGW (-O3 -fopenmp -march = native -mtune = native):

===== Call on single core:
In main: 10.57ms // 6 iterations @1.7ms each; this performs as expected
In main: 10.42ms
In main: 10.57ms
In main: 10.59ms
In main: 10.36ms
===== Call on multiple cores: 8
In main: 4.44ms
In main: 3.53ms 
In main: 3.06ms
In main: 3.16ms // roughly 3x increase with 4C/8T. Seems reasonable
In main: 3.10ms

Пример вывода MSVC (/ MD / O2 / Ob2 / openmp):

===== Call on single core:
In misc_ordered: 10.49ms
In misc_ordered: 10.43ms
In misc_ordered: 10.45ms
In misc_ordered: 11.29ms
In misc_ordered: 10.36ms
===== Call on multiple cores: 8
In misc_ordered: 3.29ms // expected
In misc_ordered: 4.02ms
In misc_ordered: 6.26ms // why??? >:-(
In misc_ordered: 6.27ms
In misc_ordered: 6.21ms

Еще раз: обратите внимание, что многократные прогоны с MSVC случайным образом дают результаты от 3 до 6 мс.

Мне кажется, что реализация openmp в MSVC пытается равномерно распределить рабочую нагрузку.Но разве планировщик Windows не должен делать это в любом случае?И если да, то почему он ведет себя по-разному на двух разных исполняемых файлах?В качестве альтернативы, возможно, некоторые потоки ожидают (все еще происходит упорядочение), но как я могу попытаться проверить и исправить это?

1 Ответ

0 голосов
/ 14 мая 2019

Оказывается, что планирование действительно отличалось между MinGW и MSVC.

  1. Планирование по умолчанию в OpenMP определяется реализацией
  2. Для этой задачи статическое расписание дает наилучшие результаты

Явная установка статического расписания и ограничение количества потоков числом итераций дает согласованные результаты для компиляторов:

#pragma omp parallel
{
        int y;
        omp_set_num_threads(min(omp_get_max_threads(), NUM_ITER));
#pragma omp for ordered schedule(static, 1)
        for(y = 0; y < NUM_ITER; y++) {
            mySlowFunc();

#pragma omp ordered
            {
                volatile int i = 0;
            }
        }
}
...