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

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

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

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

#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 прогонов, полосы ошибок (которые едва заметны) являются стандартные отклонения]: Performance as a function of thread count

График более ясен, когда отображается как скорость по отношению к OMP_NUM_THREADS = 1: Speed up as a function of thread count

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

Еще более странно, ускорение в 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

EDIT: Из любопытства я решил попробовать гораздо большее количество потоков. Моя ОС ограничивает это до 2000. Нечетные результаты (как ускорение, так и низкие потоки) говорят сами за себя! Crazy numbers of threads

EDIT: Я попробовал последнее предложение @Zboson в своем ответе, то есть поставил VZEROUPPER перед каждой математической функцией в цикле и устранил проблему масштабирования! (Он также отправил однопоточный код с 22 с до 2 с!):

correct scaling

Ответы

Ответ 1

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

Я проверил ваш код здесь http://coliru.stacked-crooked.com/a/26f4e8c9fdae5cc2

Изменить. Еще одна проблема, которая может вызвать вашу проблему, заключается в том, что функция 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 intrinsics.