Инструкции 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.

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