Я использую _mm256_fmadd_ps
для выполнения a * b
и накапливаю его до результата c
, например c=a*b+c
. Обнаружено, что при определенных обстоятельствах операции fmadd
приводят к потере точности по сравнению с теми, которые mul
сначала, а затем add
, особенно когда c
уже имеет ненулевое значение.
тестовый код:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <intrin.h>
static inline void multiply_scalar_and_accumulate_generic(float *out, const float *in, const float scalar,
unsigned int cnt)
{
const float *aPtr = (float *)in;
float *cPtr = (float *)out;
while (cnt >= 8) {
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
cnt -= 8;
}
while (cnt-- > 0) {
*cPtr = (*aPtr++) * scalar + (*cPtr); cPtr++;
}
return;
}
static inline void multiply_scalar_and_accumulate_avx(float *out, const float *in, const float scalar, unsigned int cnt)
{
unsigned int idx = 0;
const float *aPtr = (float *)in;
float *cPtr = (float *)out;
__m256 aVal;
__m256 cVal;
const __m256 bVal = _mm256_set1_ps(scalar);
for (; idx < cnt; idx += 8)
{
aVal = _mm256_loadu_ps(aPtr);
cVal = _mm256_loadu_ps(cPtr);
cVal = _mm256_add_ps(cVal, _mm256_mul_ps(aVal, bVal));
_mm256_storeu_ps(cPtr, cVal);
aPtr += 8;
cPtr += 8;
}
for (; idx < cnt; idx++)
{
*cPtr = (*aPtr++) * scalar + (*cPtr);
cPtr++;
}
return;
}
static inline void multiply_scalar_and_accumulate_avx_fma(float *out, const float *in, const float scalar,
unsigned int cnt)
{
unsigned int idx = 0;
const float *aPtr = (float *)in;
float *cPtr = (float *)out;
__m256 aVal;
__m256 cVal;
const __m256 bVal = _mm256_set1_ps(scalar);
for (; idx < cnt; idx += 8)
{
aVal = _mm256_loadu_ps(aPtr);
cVal = _mm256_loadu_ps(cPtr);
cVal = _mm256_fmadd_ps(aVal, bVal, cVal);
_mm256_storeu_ps(cPtr, cVal);
aPtr += 8;
cPtr += 8;
}
for (; idx < cnt; idx++)
{
*cPtr = (*aPtr++) * scalar + (*cPtr);
cPtr++;
}
return;
}
int main(void)
{
#define TEST_COUNT (0x4000)
float *in = NULL;
float *ref = NULL;
float *out_avx = NULL;
float *out_avx_fma = NULL;
in = (float *)malloc(sizeof(float) * TEST_COUNT);
ref = (float *)malloc(sizeof(float) * TEST_COUNT);
out_avx = (float *)malloc(sizeof(float) * TEST_COUNT);
out_avx_fma = (float *)malloc(sizeof(float) * TEST_COUNT);
if ((in == NULL) || (ref == NULL) || (out_avx == NULL) || (out_avx_fma == NULL))
{
printf("alloc failed\n");
return 0;
}
printf("test start\n");
float scalar = 0;
float diff_avx = 0;
float diff_avx_fma = 0;
const float TOLERANCE = 1e-3f;
srand(time(0));
memset(ref, 0x0, sizeof(float) * TEST_COUNT);
memset(out_avx, 0x0, sizeof(float) * TEST_COUNT);
memset(out_avx_fma, 0x0, sizeof(float) * TEST_COUNT);
for (int i = 0; i < TEST_COUNT; i++)
{
in[i] = ((float)rand()) / ((float)rand()) * 10.0f;
}
scalar = ((float)rand()) / ((float)rand()) * 10.0f;
multiply_scalar_and_accumulate_generic(ref, in, scalar, TEST_COUNT);
multiply_scalar_and_accumulate_avx(out_avx, in, scalar, TEST_COUNT);
multiply_scalar_and_accumulate_avx_fma(out_avx_fma, in, scalar, TEST_COUNT);
#define MAKE_ACCUMULATE (1)
#if MAKE_ACCUMULATE
scalar = ((float)rand()) / ((float)rand()) * 10.0f;
multiply_scalar_and_accumulate_generic(ref, in, scalar, TEST_COUNT);
multiply_scalar_and_accumulate_avx(out_avx, in, scalar, TEST_COUNT);
multiply_scalar_and_accumulate_avx_fma(out_avx_fma, in, scalar, TEST_COUNT);
#endif
for (int i = 0; i < TEST_COUNT; i++)
{
diff_avx = fabsf(out_avx[i] - ref[i]);
diff_avx_fma = fabsf(out_avx_fma[i] - ref[i]);
if (diff_avx > TOLERANCE)
{
printf("[Err AVX] pos:%06d, %20.4f != %20.4f, avx_diff:%.4f, avx_fma_diff:%.4f\n", i, out_avx[i], ref[i],
diff_avx, diff_avx_fma);
}
if (diff_avx_fma > TOLERANCE)
{
printf("[Err AVX_FMA] pos:%06d, %20.4f != %20.4f, avx_fma_diff:%.4f, avx_diff:%.4f\n", i, out_avx_fma[i], ref[i],
diff_avx_fma, diff_avx);
}
}
printf("test end\n");
return 0;
}
результат:
Ваш код не показывает никакой «потери точности»1 при использовании FMA. FMA дает лучшие результаты, и вы сравниваете их с худшими результатами, полученными при раздельном умножении и сложении.
Ваш код сравнивает результаты, вычисленные multiply_scalar_and_accumulate_avx
и multiply_scalar_and_accumulate_avx_fma
, с эталонными результатами, вычисленными с использованием простого C *
и +
, таким образом используя отдельное сложение и умножение. В этих справочных результатах каждый из *
и +
вычисляется (в вашей реализации C) отдельно с точностью float
. Эти операции имеют встроенное округление до точности float
: результат *
— это эквивалент точного арифметического произведения действительных чисел, округленного до точности float
, а результат +
— это эквивалент точной арифметической суммы действительных чисел, округленной до float
. точность.
Напротив, инструкция _mm256_fmadd_ps
имеет одно округление. Результат, который он дает для операндов a, b и c, является эквивалентом точного арифметического выражения действительных чисел a•b+c, округленного до точности float
. Этот результат всегда представляет собой значение, представленное в float
, которое ближе всего к a•b+c, поэтому это всегда наилучший возможный результат.
Это ваши справочные результаты, которые являются неточными. Поскольку у них есть два округления, их результаты иногда отличаются от лучшего результата, предоставленного FMA.
Если вы измените multiply_scalar_and_accumulate_generic
на использование double
арифметики (что можно сделать, просто изменив ее параметр const float scalar
на const double scalar
) или измените ее на использование функции fma
в стандартной библиотеке C, ваша программа сообщит, что результаты AVX
отличаются от эталонных значений. , а не результаты AVX_FMA
отличаются.
1 Это неправильное название. Точность — это точность, с которой представлены числа. Все числа в этом коде используют бинарный код IEEE-754, который имеет 24-битные мантиссы, поэтому все они имеют одинаковую точность. На самом деле вас беспокоит точность, а именно, насколько хорошо число представляет собой идеальное целевое значение.
Профессионал, я действительно проигнорировал ошибку расчета двух операций.
Вместо того, чтобы публиковать результаты в виде связанных изображений, лучше опубликовать здесь в виде текста.