Простые реализации acosf() могут легко достичь границы ошибки в 1,5 ulp по отношению к бесконечно точному (математическому) результату, если платформа имеет поддержку слияния с умножением-сложением (FMA). Это означает, что результаты никогда не отличаются более чем на одну величину от правильно округленного результата в режиме округления до ближайшего или даже четного.

Однако такая реализация обычно содержит две основные ветви кода, которые делят основной интервал аппроксимации [0,1] примерно пополам, как в приведенном ниже примерном коде. Эта ветвистость препятствует автоматической векторизации компиляторами при нацеливании на архитектуры SIMD.

Существует ли альтернативный алгоритмический подход, который легче поддается автоматической векторизации, сохраняя при этом ту же границу ошибки в 1,5 ульпс? Можно предположить поддержку платформы FMA.

/* approximate arcsin(a) on [-0.5625,+0.5625], max ulp err = 0.95080 */
float asinf_core(float a)
{
    float r, s;
    s = a * a;
    r =             0x1.a7f260p-5f;  // 5.17513156e-2
    r = fmaf (r, s, 0x1.29a5cep-6f); // 1.81669723e-2
    r = fmaf (r, s, 0x1.7f0842p-5f); // 4.67568673e-2
    r = fmaf (r, s, 0x1.329256p-4f); // 7.48465881e-2
    r = fmaf (r, s, 0x1.555728p-3f); // 1.66670144e-1
    r = r * s;
    r = fmaf (r, a, a);
    return r;
}

/* maximum error = 1.45667 ulp */
float my_acosf (float a)
{
    float r;

    r = (a > 0.0f) ? (-a) : a; // avoid modifying the "sign" of NaNs
    if (r > -0.5625f) {
        /* arccos(x) = pi/2 - arcsin(x) */
        r = fmaf (0x1.ddcb02p-1f, 0x1.aee9d6p+0f, asinf_core (r));
    } else {
        /* arccos(x) = 2 * arcsin (sqrt ((1-x) / 2)) */
        r = 2.0f * asinf_core (sqrtf (fmaf (0.5f, r, 0.5f)));
    }
    if (!(a > 0.0f) && (a >= -1.0f)) { // avoid modifying the "sign" of NaNs
        /* arccos (-x) = pi - arccos(x) */
        r = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, -r);
    }
    return r;
}
5
njuffa 3 Мар 2018 в 03:27

3 ответа

Лучший ответ

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

Основная дополнительная работа - бесполезная sqrt / fma, если все элементы имели -|a| > -0.5625f, к сожалению, на критическом пути.


Аргументом asinf_core является (r > -0.5625f) ? r : sqrtf (fmaf (0.5f, r, 0.5f)).

Параллельно с этим вы (или компилятор) можете смешивать коэффициенты для FMA на выходе.

Если вы жертвуете точностью константы pi/2, помещая ее в один float вместо создания с двумя константами-мультипликаторами в fmaf, вы можете

fmaf( condition?-1:2,  asinf_core_result,  condition ? pi/2 : 0)

Таким образом, вы выбираете между двумя константами или andps константой с SIMD-результатом сравнения, чтобы условно обнулить ее (например, x86 SSE).


Окончательное исправление основано на проверке диапазона исходного ввода, так что опять-таки есть параллелизм на уровне команд между FP-блендами и работой FMA asinf_core.

Фактически, мы можем оптимизировать это в предыдущем FMA на выходе asinf_core, смешивая постоянные входные данные с входными данными для второго условия. Мы хотим asinf_core в качестве одного из мультипликаторов для него, поэтому мы можем отрицать или нет отрицанием константы. (Реализация SIMD могла бы выполнить a_cmp = andnot( a>0.0f, a>=-1.0f), а затем multiplier ^ (-0.0f & a_cmp), где multiplier ранее было условно выполнено.

Аддитивная константа для этого FMA на выходе - 0, pi/2, pi или pi + pi/2. Учитывая два сравниваемых результата (для a и r=-|a| для случая, отличного от NaN), мы могли бы объединить это в 2-битное целое число и использовать его как элемент управления случайным образом для выбора константы FP из вектора всех 4 констант, например используя AVX vpermilps (быстрая перестановка в строке с переменным управлением). то есть вместо смешивания 4-х разных способов, используйте перемешивание в качестве 2-битной LUT !

Если мы делаем это, мы должны также сделать это для мультипликативной константы, потому что создание константы является основной ценой. Переменные бленды дороже, чем тасования на x86 (обычно 2 мопа против 1). В Skylake переменные blends (например, vblendvps) могут использовать любой порт (в то время как тасования выполняются только на порту 5). Достаточно ILP, что это, вероятно, узкие места по общей пропускной способности UOP или по всем портам ALU, а не по порту 5. (Смешивание переменных в Haswell составляет 2 моп для порта 5, поэтому оно строго хуже, чем vpermilps ymm,ymm,ymm).

Мы будем выбирать из -1, 1, -2 и 2.


Скаляр с троичными операторами , автоматически векторизует (с 8 vblendvps) с gcc7.3 -O3 -march=skylake -ffast-math. быстрое вычисление требуется для автовекторизации: / К сожалению, gcc по-прежнему использует rsqrtps + итерацию Ньютона (без FMA?!?), даже с -mrecip=none, который я думал, должен был отключить это.

Автовекторизуется только с 5 vblendvps с clang5.0 (с такими же параметрами). См как о в проводнике компилятора Godbolt . Это компилируется и выглядит как правильное количество инструкций, но в остальном не проверено.

// I think this is far more than enough digits for float precision, but wouldn't hurt to use a standard constant instead of what I typed from memory.
static const float pi_2 = 3.1415926535897932384626433 / 2;
static const float pi = 3.1415926535897932384626433;
//static const float pi_plus_pi_2 = 3.1415926535897932384626433 * 3.0 / 2;

/* maximum error UNKNOWN, completely UNTESTED */
float my_acosf_branchless (float a)
{
    float r = (a > 0.0f) ? (-a) : a; // avoid modifying the "sign" of NaNs
    bool a_in_range = !(a > 0.0f) && (a >= -1.0f);

    bool rsmall = (r > -0.5625f);
    float asinf_arg = rsmall ? r : sqrtf (fmaf (0.5f, r, 0.5f));

    float asinf_res = asinf_core(asinf_arg);

#if 0
    r = fmaf( rsmall?-1.0f:2.0f,  asinf_res,  rsmall ? pi_2 : 0);
    if (!(a > 0.0f) && (a >= -1.0f)) { // avoid modifying the "sign" of NaNs
        /* arccos (-x) = pi - arccos(x) */
        r = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, -r);
    }
#else
    float fma_mul = rsmall? -1.0f:2.0f;
    fma_mul = a_in_range ? -fma_mul : fma_mul;
    float fma_add = rsmall ? pi_2 : 0;
    fma_add = a_in_range ? fma_add + pi : fma_add;
    // to vectorize, turn the 2 conditions into a 2-bit integer.
    // Use vpermilps as a 2-bit LUT of float constants

    // clang doesn't see the LUT trick, but otherwise appears non-terrible at this blending.

    r = fmaf(asinf_res, fma_mul, fma_add);
#endif
    return r;
}

Автовекторизация протестирована с циклом, который запускает ее по массиву из 1024 выровненных float элементов; увидеть ссылку Godbolt.

TODO: встроенная версия.

2
Peter Cordes 4 Мар 2018 в 04:36

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

Кажется, что с gcc, функцию copysignf() легче векторизовать, чем троичный оператор. В следующем коде я переписал ваш скаляр решение с copysignf() вместо троичных операторов.

Код векторизован даже с довольно старым компилятором gcc 4.9, с варианты gcc -std=c99 -O3 -m64 -Wall -march=haswell -fno-math-errno. Функция sqrtf() векторизована в инструкцию vsqrtps. ссылка на Godbolt находится здесь.

#include <stdio.h>
#include <immintrin.h>
#include <math.h>

float acosf_cpsgn (float a)
{
    float r, s, t, pi2;
/*    s = (a < 0.0f) ? 2.0f : (-2.0f); */
    s = copysignf(2.0f, -a);
    t = fmaf (s, a, 2.0f);
    s = sqrtf (t);
    r =              0x1.c86000p-22f;  //  4.25032340e-7
    r = fmaf (r, t, -0x1.0258fap-19f); // -1.92483935e-6
    r = fmaf (r, t,  0x1.90c5c4p-18f); //  5.97197595e-6
    r = fmaf (r, t, -0x1.55668cp-19f); // -2.54363249e-6
    r = fmaf (r, t,  0x1.c3f78ap-16f); //  2.69393295e-5
    r = fmaf (r, t,  0x1.e8f446p-14f); //  1.16575764e-4
    r = fmaf (r, t,  0x1.6df072p-11f); //  6.97973708e-4
    r = fmaf (r, t,  0x1.3332a6p-08f); //  4.68746712e-3
    r = fmaf (r, t,  0x1.555550p-05f); //  4.16666567e-2
    r = r * t;
    r = fmaf (r, s, s);
/*    t = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, 0.0f - r); // PI-r      */
/*    r = (a < 0.0f) ? t : r;                                           */
    r = copysignf(r, a);
    pi2 = 0x1.ddcb02p+0f * 0.5f;                   /* no rounding here  */
    pi2 = pi2 - copysignf(pi2, a);                 /* no rounding here  */
    t = fmaf (pi2, 0x1.aee9d6p+0f, r);   // PI-r
    return t;
}



float my_acosf (float a)
{
    float r, s, t;
    s = (a < 0.0f) ? 2.0f : (-2.0f);
    t = fmaf (s, a, 2.0f);
    s = sqrtf (t);
    r =              0x1.c86000p-22f;  //  4.25032340e-7
    r = fmaf (r, t, -0x1.0258fap-19f); // -1.92483935e-6
    r = fmaf (r, t,  0x1.90c5c4p-18f); //  5.97197595e-6
    r = fmaf (r, t, -0x1.55668cp-19f); // -2.54363249e-6
    r = fmaf (r, t,  0x1.c3f78ap-16f); //  2.69393295e-5
    r = fmaf (r, t,  0x1.e8f446p-14f); //  1.16575764e-4
    r = fmaf (r, t,  0x1.6df072p-11f); //  6.97973708e-4
    r = fmaf (r, t,  0x1.3332a6p-08f); //  4.68746712e-3
    r = fmaf (r, t,  0x1.555550p-05f); //  4.16666567e-2
    r = r * t;
    r = fmaf (r, s, s);
    t = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, 0.0f - r); // PI-r
    r = (a < 0.0f) ? t : r;
    return r;
}


/* The code from the next 2 functions is copied from the godbold link in Peter cordes'  */
/* answer https://stackoverflow.com/a/49091530/2439725  and modified                    */
int autovec_test_a (float *__restrict dst, float *__restrict src) {
    dst = __builtin_assume_aligned(dst,32);
    src = __builtin_assume_aligned(src,32);
    for (int i=0 ; i<1024 ; i++ ) {
        dst[i] = my_acosf(src[i]);
    }
    return 0;
}

int autovec_test_b (float *__restrict dst, float *__restrict src) {
    dst = __builtin_assume_aligned(dst,32);
    src = __builtin_assume_aligned(src,32);
    for (int i=0 ; i<1024 ; i++ ) {
        dst[i] = acosf_cpsgn(src[i]);
    }
    return 0;
}
2
wim 20 Мар 2018 в 20:10

Наиболее близкое, которое я нашел к удовлетворительному решению, основано на идее из новостная рассылка Роберта Харли, в которой он заметил, что для x в [0,1], acos (x) ≈ √ (2 * (1-x)), и что полином может обеспечить масштабный коэффициент, необходимый для точного приближения по всему интервалу. Как видно из приведенного ниже кода, этот подход приводит к прямолинейному коду, использующему только два использования троичного оператора для обработки аргументов в отрицательной полуплоскости.

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

#define VECTORIZABLE 1
#define ARR_LEN      (1 << 24)
#define MAX_ULP      1 /* deviation from correctly rounded result */

#if VECTORIZABLE  
/* 
 Compute arccos(a) with a maximum error of 1.496766 ulp 
 This uses an idea from Robert Harley's posting in comp.arch.arithmetic on 1996/07/12
 https://groups.google.com/forum/#!original/comp.arch.arithmetic/wqCPkCCXqWs/T9qCkHtGE2YJ
*/
float my_acosf (float a)
{
    float r, s, t;
    s = (a < 0.0f) ? 2.0f : (-2.0f);
    t = fmaf (s, a, 2.0f);
    s = sqrtf (t);
    r =              0x1.c86000p-22f;  //  4.25032340e-7
    r = fmaf (r, t, -0x1.0258fap-19f); // -1.92483935e-6
    r = fmaf (r, t,  0x1.90c5c4p-18f); //  5.97197595e-6
    r = fmaf (r, t, -0x1.55668cp-19f); // -2.54363249e-6
    r = fmaf (r, t,  0x1.c3f78ap-16f); //  2.69393295e-5
    r = fmaf (r, t,  0x1.e8f446p-14f); //  1.16575764e-4
    r = fmaf (r, t,  0x1.6df072p-11f); //  6.97973708e-4
    r = fmaf (r, t,  0x1.3332a6p-08f); //  4.68746712e-3
    r = fmaf (r, t,  0x1.555550p-05f); //  4.16666567e-2
    r = r * t;
    r = fmaf (r, s, s);
    t = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, 0.0f - r); // PI-r
    r = (a < 0.0f) ? t : r;
    return r;
}

#else // VECTORIZABLE

/* approximate arcsin(a) on [-0.5625,+0.5625], max ulp err = 0.95080 */
float asinf_core(float a)
{
    float r, s;
    s = a * a;
    r =             0x1.a7f260p-5f;  // 5.17513156e-2
    r = fmaf (r, s, 0x1.29a5cep-6f); // 1.81669723e-2
    r = fmaf (r, s, 0x1.7f0842p-5f); // 4.67568673e-2
    r = fmaf (r, s, 0x1.329256p-4f); // 7.48465881e-2
    r = fmaf (r, s, 0x1.555728p-3f); // 1.66670144e-1
    r = r * s;
    r = fmaf (r, a, a);
    return r;
}

/* maximum error = 1.45667 ulp */
float my_acosf (float a)
{
    float r;

    r = (a > 0.0f) ? (-a) : a; // avoid modifying the "sign" of NaNs
    if (r > -0.5625f) {
        /* arccos(x) = pi/2 - arcsin(x) */
        r = fmaf (0x1.ddcb02p-1f, 0x1.aee9d6p+0f, asinf_core (r));
    } else {
        /* arccos(x) = 2 * arcsin (sqrt ((1-x) / 2)) */
        r = 2.0f * asinf_core (sqrtf (fmaf (0.5f, r, 0.5f)));
    }
    if (!(a > 0.0f) && (a >= -1.0f)) { // avoid modifying the "sign" of NaNs
        /* arccos (-x) = pi - arccos(x) */
        r = fmaf (0x1.ddcb02p+0f, 0x1.aee9d6p+0f, -r);
    }
    return r;
}
#endif // VECTORIZABLE

int main (void)
{
    double darg, dref;
    float ref, *a, *b;
    uint32_t argi, resi, refi;

    printf ("%svectorizable implementation of acos\n", 
            VECTORIZABLE ? "" : "non-");

    a = (float *)malloc (sizeof(a[0]) * ARR_LEN);
    b = (float *)malloc (sizeof(b[0]) * ARR_LEN);

    argi = 0x00000000;
    do {

        for (int i = 0; i < ARR_LEN; i++) {
            memcpy (&a[i], &argi, sizeof(a[i]));
            argi++;
        }

        for (int i = 0; i < ARR_LEN; i++) {
            b[i] = my_acosf (a[i]);
        }

        for (int i = 0; i < ARR_LEN; i++) {
            darg = (double)a[i];
            dref = acos (darg);
            ref = (float)dref;
            memcpy (&refi, &ref, sizeof(refi));
            memcpy (&resi, &b[i], sizeof(resi));
            if (llabs ((long long int)resi - (long long int)refi) > MAX_ULP) {
                printf ("error > 1 ulp a[i]=% 14.6a  b[i]=% 14.6a  ref=% 14.6a  dref=% 21.13a\n", 
                        a[i], b[i], ref, dref);
                printf ("test FAILED\n");

                return EXIT_FAILURE;
            }
        }

        printf ("^^^^ argi = %08x\n", argi);
    } while (argi);

    printf ("test PASSED\n");

    free (a);
    free (b);

    return EXIT_SUCCESS;
}

Хотя структура этого кода, по-видимому, способствует автоматической векторизации, мне не очень повезло, когда я нацелился на AVX2 с помощью компиляторов, предлагаемых Проводник компилятора. Единственный компилятор, который может векторизовать этот код в контексте внутреннего цикла моего тестового приложения выше, это Clang. Но Clang, похоже, сможет сделать это, только если я укажу -ffast-math, что, однако, имеет нежелательный побочный эффект превращения вызова sqrtf() в приблизительный квадратный корень, вычисленный с помощью rsqrt , Я экспериментировал с некоторыми менее навязчивыми переключателями, например -fno-honor-nans, -fno-math-errno, -fno-trapping-math, но my_acosf() не векторизовались, даже когда я использовал их в комбинации.

Сейчас я прибегаю к ручному переводу приведенного выше кода в AVX2 + FMA встроенные функции, например так:

#include "immintrin.h"

/* maximum error = 1.496766 ulp */
__m256 _mm256_acos_ps (__m256 x)
{
    const __m256 zero= _mm256_set1_ps ( 0.0f);
    const __m256 two = _mm256_set1_ps ( 2.0f);
    const __m256 mtwo= _mm256_set1_ps (-2.0f);
    const __m256 c0  = _mm256_set1_ps ( 0x1.c86000p-22f); //  4.25032340e-7
    const __m256 c1  = _mm256_set1_ps (-0x1.0258fap-19f); // -1.92483935e-6
    const __m256 c2  = _mm256_set1_ps ( 0x1.90c5c4p-18f); //  5.97197595e-6
    const __m256 c3  = _mm256_set1_ps (-0x1.55668cp-19f); // -2.54363249e-6
    const __m256 c4  = _mm256_set1_ps ( 0x1.c3f78ap-16f); //  2.69393295e-5
    const __m256 c5  = _mm256_set1_ps ( 0x1.e8f446p-14f); //  1.16575764e-4
    const __m256 c6  = _mm256_set1_ps ( 0x1.6df072p-11f); //  6.97973708e-4
    const __m256 c7  = _mm256_set1_ps ( 0x1.3332a6p-8f);  //  4.68746712e-3
    const __m256 c8  = _mm256_set1_ps ( 0x1.555550p-5f);  //  4.16666567e-2
    const __m256 pi0 = _mm256_set1_ps ( 0x1.ddcb02p+0f);  //  1.86637890e+0
    const __m256 pi1 = _mm256_set1_ps ( 0x1.aee9d6p+0f);  //  1.68325555e+0
    __m256 s, r, t, m;

    s = two;
    t = mtwo;
    m = _mm256_cmp_ps (x, zero, _CMP_LT_OQ);
    t = _mm256_blendv_ps (t, s, m);
    t = _mm256_fmadd_ps (x, t, s);
    s = _mm256_sqrt_ps (t);
    r = c0;
    r = _mm256_fmadd_ps (r, t, c1);
    r = _mm256_fmadd_ps (r, t, c2);
    r = _mm256_fmadd_ps (r, t, c3);
    r = _mm256_fmadd_ps (r, t, c4);
    r = _mm256_fmadd_ps (r, t, c5);
    r = _mm256_fmadd_ps (r, t, c6);
    r = _mm256_fmadd_ps (r, t, c7);
    r = _mm256_fmadd_ps (r, t, c8);
    r = _mm256_mul_ps (r, t);
    r = _mm256_fmadd_ps (r, s, s);
    t = _mm256_sub_ps (zero, r);
    t = _mm256_fmadd_ps (pi0, pi1, t);
    r = _mm256_blendv_ps (r, t, m);
    return r;
}
4
njuffa 8 Мар 2018 в 04:46