Почему «_mm256_fmadd_ps» вызывает потерю точности?

Я использую _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;
}

результат:

MAKE_ACCUMULATE == 0

MAKE_ACCUMULATE == 1

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

chux - Reinstate Monica 08.08.2024 14:42
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Ваш код не показывает никакой «потери точности»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-битные мантиссы, поэтому все они имеют одинаковую точность. На самом деле вас беспокоит точность, а именно, насколько хорошо число представляет собой идеальное целевое значение.

Профессионал, я действительно проигнорировал ошибку расчета двух операций.

Y-Jiechao 09.08.2024 03:10

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