Как использовать инструкции Fused Multiply-Add (FMA) с SSE/AVX

Я узнал, что некоторые процессоры Intel/AMD могут делать одновременное умножение и добавлять с помощью SSE/AVX:
FLOPS за цикл для песчаного моста и haswell SSE2/AVX/AVX2.

Мне нравится знать, как сделать это лучше всего в коде, и я также хочу знать, как это делается внутри CPU. Я имею в виду суперскалярную архитектуру. Скажем, я хочу сделать длинную сумму, такую ​​как следующее в SSE:

//sum = a1*b1 + a2*b2 + a3*b3 +... where a is a scalar and b is a SIMD vector (e.g. from matrix multiplication)
sum = _mm_set1_ps(0.0f);
a1  = _mm_set1_ps(a[0]); 
b1  = _mm_load_ps(&b[0]);
sum = _mm_add_ps(sum, _mm_mul_ps(a1, b1));

a2  = _mm_set1_ps(a[1]); 
b2  = _mm_load_ps(&b[4]);
sum = _mm_add_ps(sum, _mm_mul_ps(a2, b2));

a3  = _mm_set1_ps(a[2]); 
b3  = _mm_load_ps(&b[8]);
sum = _mm_add_ps(sum, _mm_mul_ps(a3, b3));
...

Мой вопрос в том, как это преобразуется в одновременное умножение и добавление? Могут ли данные быть зависимыми? Я имею в виду, может ли процессор делать _mm_add_ps(sum, _mm_mul_ps(a1, b1)) одновременно или делать регистры, используемые в умножении, и добавлять должны быть независимыми?

Наконец, как это относится к FMA (с Haswell)? Является ли _mm_add_ps(sum, _mm_mul_ps(a1, b1)) автоматически преобразовывается в одну инструкцию FMA или микрооперацию?

Ответ 1

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

FMA имеет только одно округление (оно эффективно сохраняет бесконечную точность для внутреннего временного результата умножения), тогда как ADD + MUL имеет два.

Стандарты IEEE и C позволяют это, когда действует #pragma STDC FP_CONTRACT ON, и компиляторам разрешено иметь его ON по умолчанию (но не все), По умолчанию Gcc заключает контракт в FMA (по умолчанию -std=gnu*, но не -std=c*, например -std=c++14). Для Clang он включен только с -ffp-contract=fast. (Только с включенным #pragma, только внутри одного выражения, такого как a+b*c, а не через отдельные инструкции С++.).

Это отличается от строгой или расслабленной с плавающей запятой (или в условиях gcc, -ffast-math vs. -fno-fast-math), что позволило бы другие виды оптимизации которые могли бы увеличить округление ошибка в зависимости от входных значений. Это особенное из-за бесконечной точности внутреннего временного FMA; если бы было какое-либо округление во внутреннем временном, это не было бы разрешено в строгом FP.

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


Итак, лучший способ, чтобы убедиться, что вы действительно получаете нужные вам инструкции FMA, вы фактически используете предоставленные им встроенные функции:

FMA3 Intrinsics: (AVX2 - Intel Haswell)

  • _mm_fmadd_pd(), _ mm256_fmadd_pd()
  • _mm_fmadd_ps(), _mm256_fmadd_ps()
  • и около gazillion другие варианты...

FMA4 Intrinsics: (XOP - AMD Bulldozer)

  • _mm_macc_pd(), _mm256_macc_pd()
  • _mm_macc_ps(), _mm256_macc_ps()
  • и около gazillion другие варианты...

Ответ 2

Я тестировал следующий код в GCC 5.3, Clang 3.7, ICC 13.0.1 и MSVC 2015 (версия компилятора 19.00).

float mul_add(float a, float b, float c) {
    return a*b + c;
}

__m256 mul_addv(__m256 a, __m256 b, __m256 c) {
    return _mm256_add_ps(_mm256_mul_ps(a, b), c);
}

С правильными параметрами компилятора (см. ниже) каждый компилятор сгенерирует команду vfmadd (например, vfmadd213ss) из mul_add. Однако только MSVC не сжимает mul_addv до одной инструкции vfmadd (например, vfmadd213ps).

Следующие параметры компилятора достаточны для создания инструкций vfmadd (кроме mul_addv с MSVC).

GCC:   -O2 -mavx2 -mfma
Clang: -O1 -mavx2 -mfma -ffp-contract=fast
ICC:   -O1 -march=core-avx2
MSVC:  /O1 /arch:AVX2 /fp:fast

GCC 4.9 не будет заключать mul_addv в одну инструкцию fma, но, как минимум, GCC 5.1. Я не знаю, когда другие компиляторы начали это делать.