Инструкции Intel FMA предлагают преимущество нулевой производительности
Рассмотрим следующую последовательность команд, используя инструкции Haswell FMA:
__m256 r1 = _mm256_xor_ps (r1, r1);
r1 = _mm256_fmadd_ps (rp1, m6, r1);
r1 = _mm256_fmadd_ps (rp2, m7, r1);
r1 = _mm256_fmadd_ps (rp3, m8, r1);
__m256 r2 = _mm256_xor_ps (r2, r2);
r2 = _mm256_fmadd_ps (rp1, m3, r2);
r2 = _mm256_fmadd_ps (rp2, m4, r2);
r2 = _mm256_fmadd_ps (rp3, m5, r2);
__m256 r3 = _mm256_xor_ps (r3, r3);
r3 = _mm256_fmadd_ps (rp1, m0, r3);
r3 = _mm256_fmadd_ps (rp2, m1, r3);
r3 = _mm256_fmadd_ps (rp3, m2, r3);
То же самое вычисление может быть выражено с использованием инструкций не-FMA следующим образом:
__m256 i1 = _mm256_mul_ps (rp1, m6);
__m256 i2 = _mm256_mul_ps (rp2, m7);
__m256 i3 = _mm256_mul_ps (rp3, m8);
__m256 r1 = _mm256_xor_ps (r1, r1);
r1 = _mm256_add_ps (i1, i2);
r1 = _mm256_add_ps (r1, i3);
i1 = _mm256_mul_ps (rp1, m3);
i2 = _mm256_mul_ps (rp2, m4);
i3 = _mm256_mul_ps (rp3, m5);
__m256 r2 = _mm256_xor_ps (r2, r2);
r2 = _mm256_add_ps (i1, i2);
r2 = _mm256_add_ps (r2, i3);
i1 = _mm256_mul_ps (rp1, m0);
i2 = _mm256_mul_ps (rp2, m1);
i3 = _mm256_mul_ps (rp3, m2);
__m256 r3 = _mm256_xor_ps (r3, r3);
r3 = _mm256_add_ps (i1, i2);
r3 = _mm256_add_ps (r3, i3);
Можно было бы ожидать, что версия FMA обеспечит некоторое преимущество в производительности по сравнению с версией без FMA.
Но, к сожалению, в этом случае наблюдается нулевое (0) улучшение производительности.
Может кто-нибудь помочь мне понять, почему?
Я измерил оба подхода на основной машине на базе i7-4790.
ОБНОВИТЬ:
Поэтому я проанализировал сгенерированный машинный код и определил, что компилятор MSFT VS2013 C++ генерирует машинный код так, что цепочки зависимостей r1 и r2 могут отправляться параллельно, поскольку у Haswell есть 2 канала FMA.
r3 должен отправляться после r1, поэтому в этом случае второй канал FMA простаивает.
Я думал, что если я разверну цикл, чтобы сделать 6 наборов FMA вместо 3, то я мог бы держать все каналы FMA занятыми на каждой итерации.
К сожалению, когда я проверил дамп сборки в этом случае, компилятор MSFT не выбрал назначения регистров, которые позволили бы тип параллельной диспетчеризации, который я искал, и я убедился, что я не получил увеличение производительности, которое я искал за.
Есть ли способ, которым я могу изменить свой код C (используя встроенные функции), чтобы компилятор мог генерировать лучший код?
2 ответа
Вы не предоставили полный пример кода, который включает в себя окружающий цикл (предположительно, есть окружающий цикл), поэтому трудно ответить однозначно, но главная проблема, которую я вижу, состоит в том, что задержка цепочек зависимостей вашего кода FMA значительно длиннее, чем ваш код умножения + сложения.
Каждый из трех блоков в вашем коде FMA выполняет одну и ту же независимую операцию:
TOTAL += A1 * B1;
TOTAL += A2 * B2;
TOTAL += A3 * B3;
Поскольку это структурировано, каждая операция зависит от предыдущего срока, так как каждая читает и записывает общее количество. Таким образом, задержка этой строки операции составляет 3 операции × 5 циклов /FMA = 15 циклов.
В вашей переписанной версии без FMA цепочка зависимостей на TOTAL
теперь сломан, так как вы сделали:
TOTAL_1 = A1 * B1; # 1
TOTAL_2 = A2 * B2; # 2
TOTAL_3 = A3 * B3; # 3
TOTAL_1_2 = TOTAL_1 + TOTAL2; # 5, depends on 1,2
TOTAL = TOTAL_1_2 + TOTAL3; # 6, depends on 3,5
Первые три инструкции MUL могут выполняться независимо, поскольку они не имеют никаких зависимостей. Две инструкции сложения последовательно зависят от умножения. Таким образом, задержка этой последовательности составляет 5 + 3 + 3 = 11.
Таким образом, задержка второго метода ниже, даже если он использует больше ресурсов ЦП (всего 5 выданных инструкций). Тогда, конечно, возможно, что в зависимости от того, как структурирован весь цикл, более низкая задержка сводит на нет преимущества FMA в пропускной способности для этого кода - если он хотя бы частично связан с задержкой.
Для более всестороннего статического анализа я настоятельно рекомендую IACA от Intel, который может выполнить итерацию цикла, как описано выше, и точно сказать, что является узким местом, по крайней мере, в лучшем случае. Он может идентифицировать критические пути в цикле, независимо от того, связаны ли вы с задержкой и т. Д.
Другая возможность состоит в том, что вы ограничены памятью (задержка или пропускная способность), в которой вы также увидите похожее поведение для FMA против MUL + ADD.
re: your edit: Ваш код имеет три цепочки зависимостей (r1, r2 и r3), поэтому он может поддерживать три FMA одновременно. FMA на Haswell имеет задержку 5 с, по одному на пропускную способность 0,5 с, поэтому машина может выдержать 10 FMA в полете.
Если ваш код находится в цикле, и входные данные для одной итерации не генерируются предыдущей итерацией, то вы могли бы получить 10 FMA в полете таким образом. (т. е. нет цепочки зависимостей с переносом по петле, включающей FMA). Но поскольку вы не видите увеличения производительности, вероятно, существует цепочка деп, в результате чего пропускная способность ограничивается задержкой.
Вы не опубликовали ASM, получаемый от MSVC, но вы заявляете что-то о назначениях регистра. xorps same,same
это распознанная идиома обнуления, которая запускает новую цепочку зависимостей, точно так же, как использование регистра в качестве операнда только для записи (например, назначение инструкции AVX не-FMA).
Маловероятно, что код мог бы быть правильным, но все же содержать зависимость r3 от r1. Убедитесь, что вы понимаете, что внеочередное выполнение с переименованием регистра позволяет отдельным цепочкам зависимостей использовать один и тот же регистр.
Кстати, вместо __m256 r1 = _mm256_xor_ps (r1, r1);
, вы должны использовать __m256 r1 = _mm256_setzero_ps();
, Вы должны избегать использования переменной, которую вы объявляете в своем собственном инициализаторе! Компиляторы иногда создают глупый код, когда вы используете неинициализированные векторы, например, загружаете мусор из стековой памяти или делаете дополнительные xorps
,
Еще лучше было бы:
__m256 r1 = _mm256_mul_ps (rp1, m6);
r1 = _mm256_fmadd_ps (rp2, m7, r1);
r1 = _mm256_fmadd_ps (rp3, m8, r1);
Это позволяет избежать необходимости xorps
обнулить рег для аккумулятора.
На Бродвеле, mulps
имеет меньшую задержку, чем FMA.
На Skylake FMA/mul/add имеют задержку 4c, по одному на пропускную способность 0.5c. Они сбросили отдельный сумматор с порта 1 и сделали это на устройстве FMA. Они сбрили цикл задержки от блока FMA.