在 x86(使用 SSE2)和 ARM(使用 vfpv4 NEON)上尾数为 11 位的 atan2 近似值

atan2 approximation with 11bits in mantissa on x86(with SSE2) and ARM(with vfpv4 NEON)

我正在尝试实现尾数精度为 11 位的快速 atan2(float)。 atan2 实现将用于图像处理。 所以最好用 SIMD 指令实现(The impl targeting x86(with SSE2) & ARM(with vpfv4 NEON))。

现在,我使用切比雪夫多项式逼近(https://jp.mathworks.com/help/fixedpoint/examples/calculate-fixed-point-arctangent.html)。

我愿意手动实现矢量化代码。 我将使用 SSE2(或更高版本)和 NEON 包装器库。 我计划或尝试了这些实施方案

其他好主意?

您要检查的第一件事是您的编译器在应用于 float 数组时是否能够向量化 atan2f (y,x)。这通常至少需要一个高优化级别,例如 -O3 并可能指定一个宽松的 "fast math" 模式,其中 errno 处理、非正规化和特殊输入(例如无穷大和 NaN)在很大程度上被忽略。使用这种方法,准确性可能远远超过要求,但在性能方面可能很难击败经过仔细调整的库实现。

接下来要尝试的是编写一个足够准确的简单标量实现,并让编译器对其进行矢量化。通常这意味着除了可以通过 if 转换转换为无分支代码的非常简单的分支之外,避免任何分支。此类代码的一个示例是下面显示的 fast_atan2f()。使用 Intel 编译器,作为 icl /O3 /fp:precise /Qvec_report=2 my_atan2f.c 调用,成功矢量化:my_atan2f.c(67): (col. 9) remark: LOOP WAS VECTORIZED. 通过反汇编仔细检查生成的代码表明 fast_atan2f() 已使用 [=20] 的 SSE 指令进行内联和矢量化=] 风味。

如果一切都失败了,您可以手动将 fast_atan2() 的代码翻译成特定于平台的 SIMD 内在函数,这应该不难做到。我没有足够的经验来快速完成它。

我在这段代码中使用了一个非常简单的算法,这是一个简单的参数约简以在 [0,1] 中产生一个约简参数,然后是一个极小极大多项式近似,最后一步将结果映射回完整的圆圈。核心近似是使用 Remez 算法生成的,并使用二阶 Horner 方案进行评估。如果可用,可以使用融合乘加 (FMA)。为了性能起见,不处理涉及无穷大、NaN 或 0/0 的特殊情况。

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

/* maximum relative error about 3.6e-5 */
float fast_atan2f (float y, float x)
{
    float a, r, s, t, c, q, ax, ay, mx, mn;
    ax = fabsf (x);
    ay = fabsf (y);
    mx = fmaxf (ay, ax);
    mn = fminf (ay, ax);
    a = mn / mx;
    /* Minimax polynomial approximation to atan(a) on [0,1] */
    s = a * a;
    c = s * a;
    q = s * s;
    r =  0.024840285f * q + 0.18681418f;
    t = -0.094097948f * q - 0.33213072f;
    r = r * s + t;
    r = r * c + a;
    /* Map to full circle */
    if (ay > ax) r = 1.57079637f - r;
    if (x <   0) r = 3.14159274f - r;
    if (y <   0) r = -r;
    return r;
}

/* Fixes via: Greg Rose, KISS: A Bit Too Simple. http://eprint.iacr.org/2011/007 */
static unsigned int z=362436069,w=521288629,jsr=362436069,jcong=123456789;
#define znew (z=36969*(z&0xffff)+(z>>16))
#define wnew (w=18000*(w&0xffff)+(w>>16))
#define MWC  ((znew<<16)+wnew)
#define SHR3 (jsr^=(jsr<<13),jsr^=(jsr>>17),jsr^=(jsr<<5)) /* 2^32-1 */
#define CONG (jcong=69069*jcong+13579)                     /* 2^32 */
#define KISS ((MWC^CONG)+SHR3)

float rand_float(void)
{
    volatile union {
        float f;
        unsigned int i;
    } cvt;
    do {
        cvt.i = KISS;
    } while (isnan(cvt.f) || isinf (cvt.f) || (fabsf (cvt.f) < powf (2.0f, -126)));
    return cvt.f;
}

int main (void)
{
    const int N = 10000;
    const int M = 100000;
    float ref, relerr, maxrelerr = 0.0f;
    float arga[N], argb[N], res[N];
    int i, j;

    printf ("testing atan2() with %d test vectors\n", N*M);

    for (j = 0; j < M; j++) {
        for (i = 0; i < N; i++) {
            arga[i] = rand_float();
            argb[i] = rand_float();
        }

        // This loop should be vectorized
        for (i = 0; i < N; i++) {
            res[i] = fast_atan2f (argb[i], arga[i]);
        }

        for (i = 0; i < N; i++) {
            ref = (float) atan2 ((double)argb[i], (double)arga[i]);
            relerr = (res[i] - ref) / ref;
            if ((fabsf (relerr) > maxrelerr) && 
                (fabsf (ref) >= powf (2.0f, -126))) { // result not denormal
                maxrelerr = fabsf (relerr);
            }
        }
    };

    printf ("max rel err = % 15.8e\n\n", maxrelerr);

    printf ("atan2(1,0)  = % 15.8e\n", fast_atan2f(1,0));
    printf ("atan2(-1,0) = % 15.8e\n", fast_atan2f(-1,0));
    printf ("atan2(0,1)  = % 15.8e\n", fast_atan2f(0,1));
    printf ("atan2(0,-1) = % 15.8e\n", fast_atan2f(0,-1));
    return EXIT_SUCCESS;
}

以上程序的输出应类似于以下内容:

testing atan2() with 1000000000 test vectors
max rel err =  3.53486939e-005

atan2(1,0)  =  1.57079637e+000
atan2(-1,0) = -1.57079637e+000
atan2(0,1)  =  0.00000000e+000
atan2(0,-1) =  3.14159274e+000