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

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

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

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

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

который показывает, как это можно сделать на C++. Итак, проблема в том, что мой код не будет автоматически векторизован с помощью компилятора 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);

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

Развертывание цикла для достижения максимальной пропускной способности с 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, о которой я говорю?

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

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


person iksemyonov    schedule 04.01.2015    source источник
comment
Я не могу сказать из вашего вопроса, знаете ли вы, что сам процессор способен переупорядочивать инструкции. Таким образом, добавление не должно быть рядом с умножением. Кроме того, узким местом в вашем коде будут нагрузки. Таким образом, вы все равно не получите многого от перекрывающихся добавлений и умножений.   -  person Mysticial    schedule 04.01.2015
comment
Да, я знаю, что ЦП может переупорядочивать инструкции, но не когда и как именно он это сделает. Я знаю, что память — это самая важная часть алгоритма, конечно, но когда с памятью более или менее все в порядке, я хотел бы быть уверен, что FPU работает на полную катушку, верно?   -  person iksemyonov    schedule 04.01.2015
comment
В вашем примере FPU не может работать на полную мощность. Sandy Bridge может выдерживать только одну загрузку AVX в каждом цикле. Таким образом, цикл занимает минимум 6 циклов. Чтобы насытить FPU, вам нужно 6 сложений и 6 умножений. Но у вас есть только 3 каждого из них, поэтому вы никогда не получите пропускную способность FPU более 50%.   -  person Mysticial    schedule 04.01.2015
comment
Хм. Теперь я немного потерян. Не могли бы вы проверить ссылку на вопрос SO, которую я разместил? Говорят, что 3 раза дают лучшую производительность. Кроме того, в статье Colfax утверждается, что вы можете одновременно выполнять одно сложение и одно умножение на SB. Дополнение: Подождите, понял, речь идет о загрузке и обработке данных. Спасибо за совет!   -  person iksemyonov    schedule 04.01.2015
comment
Это не имеет ничего общего с фактором развертывания. У вас просто слишком много нагрузки. Песчаный мост, выдерживает 1 нагрузку, 1 добавление и 1 умножение в каждом цикле. Но вам нужно 2 загрузки, 1 добавить и 1 умножить. Итак, ваше узкое место — это нагрузки.   -  person Mysticial    schedule 04.01.2015
comment
См. выше - я неправильно понял ваш комментарий. Кроме того, почему на диаграмме Intel SB (не могу сослаться на нее прямо сейчас - она ​​была в основном университете) показаны 2 порта, помеченные как нагрузка? Я тоже это неправильно понимаю?   -  person iksemyonov    schedule 04.01.2015
comment
Эм, должно быть ошибка. Автоматика :)   -  person iksemyonov    schedule 04.01.2015
comment
Порты 2 и 3 на SB выполняют 128-битную загрузку. Вы должны использовать оба порта, чтобы получить одну 256-битную нагрузку. Haswell может выполнять две 256-битные загрузки за такт.   -  person Z boson    schedule 05.01.2015
comment
Если вы посмотрите на код в моей ссылке, на которую вы ссылаетесь, вы увидите, что один из факторов является постоянным в цикле (__m256 a8 = _mm256_set1_ps(1.0f);). Если вы определяете __aa1 = _mm256_load_pd((a_addr)); вне своего цикла (или транслируете значение, что, вероятно, вы действительно хотите сделать), то у вас будет только одна 256-битная загрузка на мульти-добавку вместо двух. Конечно, это изменит то, что вы делаете, поэтому вам нужно подумать о том, что вы хотите сделать, и посмотреть, возможно ли это.   -  person Z boson    schedule 05.01.2015
comment
Странно, что ICC не использует выровненные нагрузки, которые вы использовали со встроенными функциями. Это предотвращает слияние нескольких инструкций загрузки. Я бы попробовал это на GCC и сравнил сборку и производительность.   -  person Z boson    schedule 05.01.2015
comment
Z boson, не могли бы вы изложить свои идеи в ответе (против встроенного комментария), чтобы они не потерялись? Где я могу найти ссылку на размер порта? Все, что у меня есть, это основной доклад Intel без указанных размеров, и я пока не гуглил его. Я также был бы признателен, если бы вы подробно рассказали о плавном умножении нагрузки: что на самом деле объединяется, загружает + множит или добавляет + множит?   -  person iksemyonov    schedule 05.01.2015
comment
Да, позже я понял, что одна из переменных была загружена только один раз, вне цикла. Может быть, выравнивание связано с выравниванием массива? В настоящее время загрузки выполняются прямо из аргумента функции - что, если я переупаковаю массив в меньший, но явно выровненный.   -  person iksemyonov    schedule 05.01.2015
comment
Постараюсь завтра дать ответ. В последнее время я завален на работе :-(   -  person Z boson    schedule 05.01.2015
comment
Конечно, то же самое здесь, нон-стоп исследования   -  person iksemyonov    schedule 05.01.2015
comment
@Zboson На Sandy Bridge невыровненная загрузка/сохранение выполняется не медленнее, чем выровненная загрузка/сохранение, когда адрес выровнен. Таким образом, больше нет причин использовать выровненную загрузку/хранилище. Кроме того, операнды памяти больше не нужно выравнивать. Лично я бы предпочел, чтобы он использовал выровненную загрузку/сохранение, так как я сразу узнаю, если мои данные не выровнены. Современные компиляторы не объединяют многократное добавление, если вы используете встроенные функции, поскольку это нарушает IEEE и предполагает, что вы уже знаете, что делаете.   -  person Mysticial    schedule 06.01.2015
comment
@Mysticial, я не имел в виду объединение умножения-сложения, я имел в виду объединение умножения и загрузки (слияние микроопераций). Я видел разные результаты с и без него stackoverflow.com/questions/21134279/. Насколько я могу судить, слияние не происходит с невыровненными нагрузками. Таким образом, с точки зрения этой инструкции больше не может быть штрафа за использование невыровненного хранилища, но это не означает, что оно не имеет других эффектов.   -  person Z boson    schedule 06.01.2015
comment
@Mysticial, я более внимательно посмотрел на сборку OP. Оказывается, он использует объединенные мультинагрузки (vmulpd ymm4, ymm3, YMMWORD PTR [r8+r14*8]). Таким образом, он использует выровненные нагрузки для объединенных множественных нагрузок и невыровненные нагрузки, когда это только нагрузка.   -  person Z boson    schedule 06.01.2015
comment
@Zboson Подожди, я не понимаю. Нет такой вещи, как выровненное умножение с операндом в памяти. Они все неравные. (для AVX)   -  person Mysticial    schedule 06.01.2015
comment
@Mysticial, вы имеете в виду, что в (vmulpd ymm4, ymm3, YMMWORD PTR [r8+r14*8]) этот YMMWORD PTR [r8+r14*8] не должен быть выровнен по 32 байтам?   -  person Z boson    schedule 06.01.2015
comment
@Mysticial по моей ссылке здесь stackoverflow.com/questions/21134279/ ясно, что MSVC объединил загрузку и множественность, хотя я не использовал выровненную загрузку, поэтому я думаю, что это говорит о том, что это идет не нужно выравнивать, как вы говорите. GCC, однако, никогда не выполняет мультиплексирование с плавной загрузкой, если вы используете встроенные функции невыровненной загрузки. Я не знаю, почему это так.   -  person Z boson    schedule 06.01.2015


Ответы (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 — константа. Почему ЦП не оптимизирован для скалярного произведения? Я могу предположить. Одной из вещей, для которых оптимизированы процессоры, является BLAS. BLAS рассматривает возможность использования в качестве строительного блока многих других подпрограмм.

Подпрограммы BLAS уровня 1 и уровня 2 ограничивают пропускную способность памяти по мере увеличения n. Только подпрограммы уровня 3 (например, умножение матриц) могут быть привязаны к вычислениям. Это связано с тем, что вычисления уровня 3 идут как n^3, а чтение как n^2. Таким образом, ЦП оптимизирован для подпрограмм уровня 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-эффективное-использование-адресов-которые-отличаются-на-4096.

получение-пиковой-пропускной-на- получает-только-в-кеше-l1-62%

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

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

person Z boson    schedule 06.01.2015
comment
Я бы не стал так далеко утверждать, что AVX512 не будет интересен, если не сможет загружать 1024 бита за такт. Матричное умножение — не единственное приложение. Материал, с которым я имею дело, имеет гораздо более высокое соотношение вычислений/нагрузки. Но учитывая, что Intel, похоже, оптимизирует процессор для линейной алгебры, было бы довольно трудно не получить двойную 512-битную нагрузку. - person Mysticial; 06.01.2015
comment
@Mysticial, ты прав. Я должен был сказать, что это не будет интересно для BLAS. Я думаю, что DGEMM является эталоном, которого ожидают многие, особенно в высокопроизводительных вычислениях (Top500). Так что для хвастовства Intel хочет двойную 512-битную загрузку. Я не знаю, хорошо ли подчеркивать BLAS для оптимизации в целом. - person Z boson; 07.01.2015
comment
Спасибо за развернутый ответ, пока нет времени изучать все ссылки, но скоро сделаю это! - person iksemyonov; 10.01.2015