【问题标题】:Accurate vectorizable implementation of acosf()acosf() 的精确矢量化实现
【发布时间】:2018-03-03 00:27:01
【问题描述】:

如果平台支持融合乘加 (FMA),acosf() 的简单实现可以轻松实现 1.5 ulp 的误差范围(相对于无限精确(数学)结果)。这意味着在舍入到最近或偶数模式下,结果与正确舍入结果的差异永远不会超过一个 ulp。

但是,这样的实现通常包括两个主要的代码分支,它们将主要近似区间 [0,1] 大致分成两半,如下面的示例代码所示。当针对 SIMD 架构时,这种分支性会抑制编译器的自动向量化。

是否有另一种算法方法更容易实现自动矢量化,同时保持 1.5 ulps 的相同误差范围?可以假设平台支持 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;
}

【问题讨论】:

    标签: algorithm math floating-point simd


    【解决方案1】:

    我最接近令人满意的解决方案是基于 Robert Harley 的 news posting 的一个想法,他在其中观察到对于 [0,1] 中的 x,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;
    }
    

    尽管这段代码的结构似乎有利于自动矢量化,但在使用Compiler Explorer 提供的编译器定位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;
    }
    

    【讨论】:

    • 试试-ffast-math -mtune=skylake;对于具有更好 sqrtps 吞吐量的 CPU,clang 在将 sqrt 重写为 rsqrt + Newton 迭代方面不太激进。 (或者 clang 支持 gcc 的 -mrecip=none 来禁用该优化。)
    • @PeterCordes 感谢您的指点。现在我已经厌倦了自动矢量化器,这是第 n 次。每隔几年,我就会用当时最新的技术尝试几个“简单”的案例,结果又一次失望了。在我处理所有可能相关的编译器选项的时间里,我能够使用 SIMD 内在函数自己编写代码(尽管一次只有一个平台,因此希望可移植代码也可以按需矢量化)。跨度>
    • 自动矢量化常常令人失望:/对于我的答案(问题中算法的无分支版本),我希望编译器在自动矢量化混合方面做得很差,并且绝对不会发现 shuffle-LUT 而不是混合优化。
    【解决方案2】:

    问题中代码的无分支版本是可能的(几乎没有任何多余的工作,只是一些比较/混合来为 FMA 创建常量),但 IDK 如果编译器会自动矢量化它。

    如果所有元素都有-|a| &gt; -0.5625f,主要的额外工作是无用的sqrt/fma,不幸的是在关键路径上。


    asinf_core 的参数是 (r &gt; -0.5625f) ? r : sqrtf (fmaf (0.5f, r, 0.5f))

    与此同时,您(或编译器)可以在输出中混合 FMA 的系数。

    如果您牺牲pi/2 常量的准确性,将其放入一个float,而不是用2 个常量被乘数创建fmaf,您可以

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

    因此,您可以在两个常量之间进行选择,或者andps 一个带有 SIMD 比较结果的常量以有条件地将其归零(例如 x86 SSE)。


    最终修复基于对原始输入的范围检查,因此在 FP 混合和 asinf_core 的 FMA 工作之间再次存在指令级并行性。

    事实上,我们可以在asinf_core 的输出上将其优化为先前的 FMA,方法是将常量输入与第二个条件下的输入混合。我们希望asinf_core 作为它的被乘数之一,所以我们可以通过否定常数来否定或不否定。 (SIMD 实现可能先执行a_cmp = andnot( a&gt;0.0f, a&gt;=-1.0f),然后执行multiplier ^ (-0.0f &amp; a_cmp),其中multiplier 之前有条件地执行。

    输出上该 FMA 的附加常数为 0pi/2pipi + pi/2。给定两个比较结果(在ar=-|a| 上,对于非 NaN 情况),我们可以将其组合成一个 2 位整数,并将其用作随机控制从向量中选择一个 FP 常数所有 4 个常量,例如使用 AVX vpermilps(带有可变控制的快速通道内随机播放)。即不是混合 4 种不同的方式,而是使用 shuffle 作为 2 位 LUT

    如果我们这样做,我们也应该为乘法常数这样做,因为创建常数是主要成本。可变混合比 x86 上的随机播放更昂贵(通常 2 微指令对 1 微指令)。在 Skylake 上,可变混合(如 vblendvps)可以使用任何端口(而 shuffle 仅在端口 5 上运行)。有足够的 ILP,这可能会成为整体 uop 吞吐量或整体 ALU 端口的瓶颈,而不是端口 5。(Haswell 上的变量混合是端口 5 的 2 uops,因此它比vpermilps ymm,ymm,ymm 更差)。

    我们将从 -1、1、-2 和 2 中进行选择。


    带有三元运算符的标量,使用 gcc7.3 -O3 -march=skylake -ffast-math 自动矢量化(带有 8 个 vblendvps)。自动向量化所需的快速数学:/ 不幸的是,gcc 仍然使用rsqrtps + 牛顿迭代(没有 FMA?!?),即使使用 -mrecip=none, which I thought was supposed to disable this

    使用 clang5.0 仅使用 5 个 vblendvps 进行自动矢量化(使用相同的选项)。参见on the Godbolt compiler explorer。这可以编译,并且看起来可能是正确数量的指令,但未经测试。

    // 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:内在函数版本。

    【讨论】:

    • 我有没有机会说服您展示我可以在 Compiler Explorer 中提供给编译器的实际代码? :-) 这比文字描述更容易阅读,也可能更短。我记得,我无法在不将acosf() 中的误差范围提高到 1.5 ulp 以上的情况下降低 PI 相关常数的准确性,这是不可取的。
    • @njuffa:当然,我会试一试。它可能对不需要 1.5 ulp 的人有用,但太糟糕了,我们可能会失去那个不错的属性。
    • @njuffa:好的,编译的标量版本。当心错误,尤其是ra 上的 4 种条件组合,如果我有任何混淆的话。
    • 我已经开始查看这个并且目前正在尝试追查以下错误的来源(首次报告):@ a = bf0fffff -5.62499940e-001 res=4.11498260e+000 ref=2.16820267e+000
    • 我之前评论中my_acosf_branchless() 的最大误差为 1.456672 ulp。在我尝试将代码按摩成可以由编译器自动矢量化的东西时,还没有运气。
    【解决方案3】:

    这不是一种完全替代的算法方法,但尽管如此 您可能会对此扩展评论感兴趣。

    看来,使用 gcc,函数 copysignf() 比 三元运算符。在下面的代码中,我重写了你的标量 solutioncopysignf() 而不是三元运算符。

    即使使用相当老的 gcc 4.9 编译器,代码也会向量化,使用 选项gcc -std=c99 -O3 -m64 -Wall -march=haswell -fno-math-errnosqrtf() 函数被向量化为 vsqrtps 指令。 Godbolt link is here.

    #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;
    }
    

    【讨论】:

    • 感谢您提出可向量化习语的这一特殊方面。您的变体似乎工作正常,除了我的测试框架抱怨 NaN 结果的“符号”错误:arg= 0x1.000002p+0 res= 0x1.#QNAN0p+0 (7fc00000) ref=-0x1.#IND00p+0 (ffc00000)。我猜sqrt() 生成的 NaN INDEFINITE 在最终修复过程中被破坏了。我会考虑是否有一个简单的解决方法。
    • 我已经尝试在函数末尾插入t = copysignf(t, 1.0f - fabsf(a));,就在return t;之前。我没有为所有现有的浮点数测试它,但至少对于参数 2.0f 和 -2.0f 它有效。使用这些输入,结果为res = 0xFFC00000,与my_acosf() 相同。
    • @njuffa:你真的需要 NaN 上的正确符号吗?如果是这样,并且 NaN 很少见,那么手动矢量化版本可能会将符号副本放在 if(movmskps(cmpunordps(input,input)) != 0) 中。因此,在具有任何 NaN 的输入上进行分支,然后为具有输入 NaN 的元素复制符号(或带有 blendv 的 NaN 本身)。在没有 NaN 的正常情况下,这会将符号复制从关键路径中移除,并将其替换为 3 个微指令:vcmpeqps/vmovmskps/test+jcc。它们像所有分支一样不在关键路径上,input 上的分支可以及早发现错误预测。
    • @PeterCordes 我个人的设计理念:我总是喜欢将参考实现与高性能实现完全匹配,如果这可能没有明显的性能影响的话。如果仔细考虑的话,这通常无需创建额外的代码分支就可以实现。在这种情况下,我上面的手动矢量化 AVX2 版本完全匹配。
    猜你喜欢
    • 2016-04-29
    • 2018-06-11
    • 1970-01-01
    • 1970-01-01
    • 2018-10-12
    • 2018-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多