Неожиданно хорошая производительность с параллелью openmp для цикла

Я отредактировал свой вопрос после предыдущих комментариев (особенно @Zboson) для лучшей читаемости

Я всегда действовал и следовал общепринятому мнению, что количество потоков openmp должно примерно соответствовать числу гиперпотоков на машине для оптимальной производительности. Тем не менее, я наблюдаю странное поведение на моем новом ноутбуке с Intel Core i7 4960HQ, 4 ядра - 8 потоков. (Смотрите документы Intel здесь)

Вот мой тестовый код:

#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <omp.h>

int main() {
    const int n = 256*8192*100;
    double *A, *B;
    posix_memalign((void**)&A, 64, n*sizeof(double));
    posix_memalign((void**)&B, 64, n*sizeof(double));
    for (int i = 0; i < n; ++i) {
        A[i] = 0.1;
        B[i] = 0.0;
    }
    double start = omp_get_wtime();
    #pragma omp parallel for
    for (int i = 0; i < n; ++i) {
        B[i] = exp(A[i]) + sin(B[i]);
    }
    double end = omp_get_wtime();
    double sum = 0.0;
    for (int i = 0; i < n; ++i) {
        sum += B[i];
    }
    printf("%g %g\n", end - start, sum);
    return 0;
}

Когда я скомпилирую его, используя gcc 4.9-4.9-20140209с помощью команды: gcc -Ofast -march=native -std=c99 -fopenmp -Wa,-q Я вижу следующее исполнение при изменении OMP_NUM_THREADS [точки в среднем за 5 прогонов, полосы ошибок (которые едва различимы) являются стандартными отклонениями]:Производительность как функция количества потоков

График является более четким, когда отображается как ускорение относительно OMP_NUM_THREADS=1:Ускорение в зависимости от количества потоков

Производительность более или менее монотонно возрастает с увеличением количества потоков, даже если число потоков omp очень сильно превышает ядро, а также число гипер-потоков! Обычно производительность должна падать, когда используется слишком много потоков (по крайней мере, в моем предыдущем опыте) из-за накладных расходов на потоки. Тем более, что вычисление должно быть связано с процессором (или, по крайней мере, с памятью) и не ожидать ввода-вывода.

Еще более странно, что ускорение в 35 раз!

Кто-нибудь может объяснить это?

Я также проверил это с гораздо меньшими массивами 8192*4 и увидел аналогичное масштабирование производительности.

В случае, если это имеет значение, я нахожусь на Mac OS 10.9 и данные о производительности были получены при запуске (под Bash):

for i in {1..128}; do
    for k in {1..5}; do
        export OMP_NUM_THREADS=$i;
        echo -ne $i $k "";
        ./a.out;
    done;
done > out

РЕДАКТИРОВАТЬ: из любопытства я решил попробовать гораздо большее количество потоков. Моя операционная система ограничивает это до 2000. Странные результаты (как ускорение, так и низкие издержки потока) говорят сами за себя!Сумасшедшие номера потоков

РЕДАКТИРОВАТЬ: Я попытался @Zboson последнее предложение в их ответе, то есть поставить VZEROUPPER перед каждой математической функцией в цикле, и это действительно решило проблему масштабирования! (Он также отправил однопоточный код от 22 с до 2 с!):

правильное масштабирование

1 ответ

Решение

Проблема, скорее всего, связана с clock() функция. Это не возвращает время стены на Linux. Вы должны использовать функцию omp_get_wtime(), Это точнее, чем часы и работает на GCC, ICC и MSVC. На самом деле, я использую его для временного кода, даже когда я не использую OpenMP.

Я проверил ваш код с этим здесь /questions/10814615/dvoichnyie-dannyie-v-mysql/10814634#10814634

Изменить: Еще одна вещь, которую следует учитывать, которая может быть причиной вашей проблемы, заключается в exp а также sin Используемая вами функция компилируется БЕЗ поддержки AVX. Ваш код скомпилирован с поддержкой AVX (на самом деле AVX2). Вы можете увидеть это из GCC Explorer с вашим кодом, если вы компилируете с -fopenmp -mavx2 -mfma Каждый раз, когда вы вызываете функцию без поддержки AVX из кода с AVX, вам нужно обнулить верхнюю часть регистра YMM или заплатить большой штраф. Вы можете сделать это с внутренней _mm256_zeroupper (VZEROUPPER). Clang делает это для вас, но в последний раз я проверял, что GCC не делает этого, поэтому вы должны сделать это самостоятельно (см. Комментарии к этому вопросу. После запуска любой функции Intel AVX математические функции требуют больше циклов, а также ответ здесь. Использование инструкций процессора AVX: низкая производительность без "/ arch: AVX"). Таким образом, каждая итерация имеет большую задержку из-за отсутствия вызова VZEROUPPER. Я не уверен, почему это важно для нескольких потоков, но если GCC делает это каждый раз, когда запускает новый поток, это может помочь объяснить, что вы видите.

#include <immintrin.h>

#pragma omp parallel for
for (int i = 0; i < n; ++i) {
    _mm256_zeroupper();
    B[i] = sin(B[i]);
    _mm256_zeroupper();
    B[i] += exp(A[i]);       
}

Редактировать Более простой способ проверить это - вместо компиляции с -march=native не устанавливайте арку (gcc -Ofast -std=c99 -fopenmp -Wa) или просто используйте SSE2 (gcc -Ofast -msse2 -std=c99 -fopenmp -Wa).

Редактировать GCC 4.8 имеет опцию -mvzeroupper что может быть наиболее удобным решением.

Эта опция инструктирует GCC выдавать команду vzeroupper перед передачей потока управления из функции, чтобы минимизировать потери при переходе с AVX на SSE, а также удалить ненужные встроенные функции zeroupper.

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