Когда компилятор переупорядочивает инструкции AVX на Sandy, это влияет на производительность?

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

Я начал учиться intrinsics несколько дней назад, поэтому ответ может показаться очевидным для некоторых, но у меня нет надежного источника информации, чтобы понять это.

Мне нужно оптимизировать некоторый код для CPU Sandy Bridge (это требование). Теперь я знаю, что он может сделать один AVX умножить и добавить один AVX за цикл и прочитать эту статью:

http://research.colfaxinternational.com/file.axd?file=2012%2F7%2FColfax_CPI.pdf

который показывает, как это можно сделать на С++. Таким образом, проблема в том, что мой код не будет автоматически сгенерирован с использованием Intel-компилятора (что является еще одним требованием к задаче), поэтому я решил реализовать его вручную, используя встроенные функции следующим образом:

__sum1 = _mm256_setzero_pd();
__sum2 = _mm256_setzero_pd();
__sum3 = _mm256_setzero_pd();
sum = 0;
for(kk = k; kk < k + BS && kk < aW; kk+=12)
{
    const double *a_addr = &A[i * aW + kk];
    const double *b_addr = &newB[jj * aW + kk];
    __aa1 = _mm256_load_pd((a_addr));
    __bb1 = _mm256_load_pd((b_addr));
    __sum1 = _mm256_add_pd(__sum1, _mm256_mul_pd(__aa1, __bb1));

    __aa2 = _mm256_load_pd((a_addr + 4));
    __bb2 = _mm256_load_pd((b_addr + 4));
    __sum2 = _mm256_add_pd(__sum2, _mm256_mul_pd(__aa2, __bb2));

    __aa3 = _mm256_load_pd((a_addr + 8));
    __bb3 = _mm256_load_pd((b_addr + 8));
    __sum3 = _mm256_add_pd(__sum3, _mm256_mul_pd(__aa3, __bb3));
}
__sum1 = _mm256_add_pd(__sum1, _mm256_add_pd(__sum2, __sum3));
_mm256_store_pd(&vsum[0], __sum1);

Причина, по которой я вручную разворачиваю цикл, как это объясняется здесь:

Развертывание Loop для достижения максимальной пропускной способности с помощью Ivy Bridge и Haswell

Говорят, вам нужно разворачиваться в 3 раза, чтобы добиться лучшей производительности на Сэнди. Мое наивное тестирование подтверждает, что это действительно работает лучше, чем без разворачивания или 4-кратного разворота.

ОК, так вот проблема. Компилятор icl от Intel Parallel Studio 15 генерирует это:

    $LN149:
            movsxd    r14, r14d                                     ;78.49
    $LN150:
            vmovupd   ymm3, YMMWORD PTR [r11+r14*8]                 ;80.48
    $LN151:
            vmovupd   ymm5, YMMWORD PTR [32+r11+r14*8]              ;84.49
    $LN152:
            vmulpd    ymm4, ymm3, YMMWORD PTR [r8+r14*8]            ;82.56
    $LN153:
            vmovupd   ymm3, YMMWORD PTR [64+r11+r14*8]              ;88.49
    $LN154:
            vmulpd    ymm15, ymm5, YMMWORD PTR [32+r8+r14*8]        ;86.56
    $LN155:
            vaddpd    ymm2, ymm2, ymm4                              ;82.34
    $LN156:
            vmulpd    ymm4, ymm3, YMMWORD PTR [64+r8+r14*8]         ;90.56
    $LN157:
            vaddpd    ymm0, ymm0, ymm15                             ;86.34
    $LN158:
            vaddpd    ymm1, ymm1, ymm4                              ;90.34
    $LN159:
            add       r14d, 12                                      ;76.57
    $LN160:
            cmp       r14d, ebx                                     ;76.42
    $LN161:
            jb        .B1.19        ; Prob 82%                      ;76.42

Для меня это похоже на беспорядок, где исправлен правильный порядок (добавьте рядом с умножением, требуемым для использования удобной функции SB).

Вопрос:

  • Будет ли этот код сборки использовать функцию Sandy Bridge, о которой я говорю?

  • Если нет, что мне нужно сделать, чтобы использовать эту функцию и не допустить, чтобы код стал "запутанным" следующим образом?

Кроме того, когда есть только одна итерация цикла, порядок является хорошим и чистым, то есть загружать, умножать, добавлять, как и должно быть.

Ответы

Ответ 1

С процессорами x86 многие люди ожидают получить максимальные FLOPS от точечного продукта

for(int i=0; i<n; i++) sum += a[i]*b[i];

но это оказывается не в этом.

Что может дать максимальный FLOPS, это

for(int i=0; i<n; i++) sum += k*a[i];

где k - константа. Почему CPU не оптимизирован для точечного продукта? Я могу спекулировать. Оптимизированы для оптимизации процессоров BLAS. BLAS рассматривает строительный блок многих других процедур.

Процедуры BLAS уровня 1 и уровня 2 становятся границами полосы пропускания памяти при увеличении n. Это только подпрограммы Level-3 (например, Matrix Multiplication), которые могут быть связаны с вычислением. Это связано с тем, что вычисления Уровня 3 идут как n^3, а чтение - как n^2. Таким образом, CPU оптимизирован для подпрограмм уровня 3. Процедуры уровня 3 не нуждаются в оптимизации для одного точечного продукта. Им нужно читать только одну матрицу за итерацию (sum += k*a[i]).

Из этого можно сделать вывод, что количество бит, которые необходимо читать каждому циклу для получения максимальных FLOPS для подпрограмм уровня 3, это

read_size = SIMD_WIDTH * num_MAC

где num_MAC - это число операций умножения с накоплением, которые могут выполняться в каждом цикле.

                   SIMD_WIDTH (bits)   num_MAC  read_size (bits)  ports used
Nehalem            128                 1         128              128-bits on port 2
Sandy Bridge       256                 1         256              128-bits port 2 and 3
Haswell            256                 2         512              256-bits port 2 and 3
Skylake            512                 2        1024              ?

Для Nehalem-Haswell это согласуется с тем, на что способно аппаратное обеспечение. На самом деле я не знаю, что Skylake сможет читать 1024 бит за такт, но если он не может быть AVX512 не очень интересным, я уверен в своей догадке. Хороший участок для Nahalem, Sandy Bridge и Haswell для каждого порта можно найти на http://www.anandtech.com/show/6355/intels-haswell-architecture/8

До сих пор я игнорировал цепи задержки и зависимостей. Чтобы действительно получить максимальные FLOPS, вам нужно развернуть цикл по крайней мере три раза на Sandy Bridge (я использую четыре, потому что мне неудобно работать с краткими тремя)

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

l1-memory-bandwidth-50-drop-in-efficiency-using-addresses-which-differ-by-4096.

get-peak-bandwidth-on-haswell-in-the-l1-cache-only-getting-62%

difference-in-performance-between-msvc-and-gcc-for-highly-optimized-matrix-multp.

Я также предлагаю вам использовать IACA для изучения производительности.