从固定点atan2()近似中删除慢的int64除法

时间:2014-08-30 17:32:51

标签: c optimization fixed-point integer-division approximation

我做了一个计算atan2(y,x)的定点近似的函数。问题在于运行整个函数所需的~83个循环,70个循环(在AMD FX-6100上使用gcc 4.9.1 mingw-w64 -O3进行编译)完全由一个简单的64位整数除法完成!遗憾的是,该分裂的任何条款都不变。我能加速分裂吗?有什么办法可以删除吗?

我想我需要这个除法因为我用1D查找表近似atan2(y,x)我需要将x,y表示的点的距离标准化为单位圆或单位正方形(我选择了一个单位'钻石',它是一个旋转45°的单位正方形,在正象限上提供非常均匀的精度)。所以除法找到(| y | - | x |)/(| y | + | x |)。注意,除数是32位,而分子是32位数,右移29位,因此除法的结果有29个小数位。另外,使用浮点除法不是一个选项,因为此函数不需要使用浮点运算。

有什么想法吗?我想不出有什么可以改善这一点(我无法弄清楚为什么只需要一个师就需要70个周期)。以下是完整的参考功能:

int32_t fpatan2(int32_t y, int32_t x)       // does the equivalent of atan2(y, x)/2pi, y and x are integers, not fixed point
{
    #include "fpatan.h" // includes the atan LUT as generated by tablegen.exe, the entry bit precision (prec), LUT size power (lutsp) and how many max bits |b-a| takes (abdp)
    const uint32_t outfmt = 32; // final output format in s0.outfmt
    const uint32_t ofs=30-outfmt, ds=29, ish=ds-lutsp, ip=30-prec, tp=30+abdp-prec, tmask = (1<<ish)-1, tbd=(ish-tp);   // ds is the division shift, the shift for the index, bit precision of the interpolation, the mask, the precision for t and how to shift from p to t
    const uint32_t halfof = 1UL<<(outfmt-1);    // represents 0.5 in the output format, which since it is in turns means half a circle
    const uint32_t pds=ds-lutsp;    // division shift and post-division shift
    uint32_t lutind, p, t, d;
    int32_t a, b, xa, ya, xs, ys, div, r;

    xs = x >> 31;       // equivalent of fabs()
    xa = (x^xs) - xs;
    ys = y >> 31;
    ya = (y^ys) - ys;

    d = ya+xa;
    if (d==0)       // if both y and x are 0 then they add up to 0 and we must return 0
        return 0;

    // the following does 0.5 * (1. - (y-x) / (y+x))
    // (y+x) is u1.31, (y-x) is s0.31, div is in s1.29

    div = ((int64_t) (ya-xa)<<ds) / d;  // '/d' normalises distance to the unit diamond, immediate result of division is always <= +/-1^ds
    p = ((1UL<<ds) - div) >> 1;     // before shift the format is s2.29. position in u1.29

    lutind = p >> ish;      // index for the LUT
    t = (p & tmask) >> tbd;     // interpolator between two LUT entries

    a = fpatan_lut[lutind];
    b = fpatan_lut[lutind+1];
    r = (((b-a) * (int32_t) t) >> abdp) + (a<<ip);  // linear interpolation of a and b by t in s0.32 format

    // Quadrants
    if (xs)             // if x was negative
        r = halfof - r;     // r = 0.5 - r

    r = (r^ys) - ys;        // if y was negative then r is negated

    return r;
}

4 个答案:

答案 0 :(得分:6)

不幸的是,对于x86 CPU上的64位整数除法,通常会有70个周期的延迟。浮点除法通常具有大约一半的延迟或更少。增加的成本来自于现代CPU在其浮点执行单元中仅具有分频器(它们在硅区域方面非常昂贵),因此需要将整数转换为浮点并再返回。因此,只需用浮动除法代替整数1就不太可能有所帮助。您需要重构代码以使用浮点代替以利用更快的浮点除法。

如果您能够重构代码,那么如果您不需要确切的答案,您也可以从大致的浮点互惠指令RCPSS中受益。它的延迟大约为5个周期。

答案 1 :(得分:2)

基于@Iwillnotexist Idonotexist建议使用lzcnt,互易和乘法我实现了一个分频函数,该函数运行在大约23.3个周期内,并且具有相当高的精度,1千分之一,19百万分之一,1.5 kB LUT,例如最糟糕的情况之一是1428769848/1080138864你可能得到1.3227648959而不是1.3227649663。

我在研究这个问题时想出了一个有趣的技术,我真的很难想到能够快速和精确的东西,因为甚至不是[0.5,1.0]中的1 / x的二次近似与内插差异相结合LUT会这样做,然后我有了相反的想法,所以我做了一个查找表,其中包含适合曲线的二次系数,该短段代表[0.5,1.0]曲线的1/128,给出了一个非常小的错误like so。并且使用表示在[0.5,1.0)范围内的x的7个最高有效位作为LUT索引,直接得到对x所属的段最有效的系数。

以下是查找表ffo_lut.hfpdiv.h的完整代码:

#include "ffo_lut.h"

static INLINE int32_t log2_ffo32(uint32_t x)    // returns the number of bits up to the most significant set bit so that 2^return > x >= 2^(return-1)
{
    int32_t y;

    y = x>>21;  if (y)  return ffo_lut[y]+21;
    y = x>>10;  if (y)  return ffo_lut[y]+10;
    return ffo_lut[x];
}

// Usage note: for fixed point inputs make outfmt = desired format + format of x - format of y
// The caller must make sure not to divide by 0. Division by 0 causes a crash by negative index table lookup
static INLINE int64_t fpdiv(int32_t y, int32_t x, int32_t outfmt)   // ~23.3 cycles, max error (by division) 53.39e-9
{
    #include "fpdiv.h"  // includes the quadratic coefficients LUT (1.5 kB) as generated by tablegen.exe, the format (prec=27) and LUT size power (lutsp)
    const int32_t *c;
    int32_t xa, xs, p, sh;
    uint32_t expon, frx, lutind;
    const uint32_t ish = prec-lutsp-1, cfs = 31-prec, half = 1L<<(prec-1);  // the shift for the index, the shift for 31-bit xa, the value of 0.5
    int64_t out;
    int64_t c0, c1, c2;

    // turn x into xa (|x|) and sign of x (xs)
    xs = x >> 31;
    xa = (x^xs) - xs;

    // decompose |x| into frx * 2^expon
    expon = log2_ffo32(xa);
    frx = (xa << (31-expon)) >> cfs;    // the fractional part is now in 0.27 format

    // lookup the 3 quadratic coefficients for c2*x^2 + c1*x + c0 then compute the result
    lutind = (frx - half) >> ish;       // range becomes [0, 2^26 - 1], in other words 0.26, then >> (26-lutsp) so the index is lutsp bits
    lutind *= 3;                // 3 entries for each index
    c = &fpdiv_lut[lutind];         // c points to the correct c0, c1, c2
    c0 = c[0];    c1 = c[1];    c2 = c[2];
    p = (int64_t) frx * frx >> prec;    // x^2
    p = c2 * p >> prec;         // c2 * x^2
    p += c1 * frx >> prec;          // + c1 * x
    p += c0;                // + c0, p = (1.0 , 2.0] in 2.27 format

    // apply the necessary bit shifts and reapplies the original sign of x to make final result
    sh = expon + prec - outfmt;     // calculates the final needed shift
    out = (int64_t) y * p;          // format is s31 + 1.27 = s32.27
    if (sh >= 0)
        out >>= sh;
    else
        out <<= -sh;
    out = (out^xs) - xs;            // if x was negative then out is negated

    return out;
}

我认为~23.3周期与它的功能一样好,但是如果你有任何想法可以减少几个周期,请告诉我。

关于fpatan2()问题,解决方案是替换这一行:

div = ((int64_t) (ya-xa)<<ds) / d;

用那一行:

div = fpdiv(ya-xa, d, ds);

答案 2 :(得分:1)

你的时间生猪指令:

div = ((int64_t) (ya-xa)<<ds) / d;

至少暴露两个问题。第一个是掩盖内置div函数;但这是次要的事实,可能永远不会被观察到。第二个是,首先,根据C语言规则,两个操作数都转换为公共类型int64_t,然后,此类型的除法扩展为CPU指令,它将128位被除数除以64- bit divisor(!)从函数的简化版程序集中提取:

  21:   48 89 c2                mov    %rax,%rdx
  24:   48 c1 fa 3f             sar    $0x3f,%rdx ## this is sign bit extension
  28:   48 f7 fe                idiv   %rsi

是的,这种划分需要大约70个周期并且无法优化(嗯,实际上它可以,但是例如反向除数方法需要与192位乘积相乘)。但是如果你确定这个除法可以用64位被除数和32位除数完成并且它不会溢出(商将适合32位)(我同意因为ya-xa总是比绝对值小于ya + xa),这可以使用显式汇编请求加速:

uint64_t tmp_num = ((int64_t) (ya-xa))<<ds;
asm("idivl %[d]" :
    [a] "=a" (div1) :
    "[a]" (tmp_num), "d" (tmp_num >> 32), [d] "q" (d) :
    "cc");

这是快速和肮脏的,应该仔细核实,但我希望这个想法得到理解。生成的程序集现在看起来像:

  18:   48 98                   cltq   
  1a:   48 c1 e0 1d             shl    $0x1d,%rax
  1e:   48 89 c2                mov    %rax,%rdx
  21:   48 c1 ea 20             shr    $0x20,%rdx
  27:   f7 f9                   idiv   %ecx

这似乎是一个巨大的进步,因为根据英特尔优化手册,64/32分区在Core系列上需要多达25个时钟周期,而不是你看到的128/64分区。

可以添加更多次要批准;例如转变可以在经济上同时进行:

uint32_t diff = ya - xa;
uint32_t lowpart = diff << 29;
uint32_t highpart = diff >> 3;
asm("idivl %[d]" :
    [a] "=a" (div1) :
    "[a]" (lowpart), "d" (highpart), [d] "q" (d) :
    "cc");

导致:

  18:   89 d0                   mov    %edx,%eax
  1a:   c1 e0 1d                shl    $0x1d,%eax
  1d:   c1 ea 03                shr    $0x3,%edx
  22:   f7 f9                   idiv   %ecx

但与分部相关的修复相比,这是一个小修复。

总而言之,我真的怀疑这个例程值得用C语言实现。后者在整数运算中是非常不经济的,需要无用的扩展和高部分损失。整个例程值得转移到汇编程序。

答案 3 :(得分:-1)

鉴于fpatan()实施,您可以简单地实施fpatan2()

假设为pi abd pi / 2定义了常量:

int32_t fpatan2(int32_t y,int32_t x) {     修复了theta;

if( x == 0 )
{
    theta = y > 0 ? fixed_half_pi : -fixed_half_pi ;
}
else
{
    theta = fpatan( y / x ) ;
    if( x < 0 )
    {
        theta += ( y < 0 ) ? -fixed_pi : fixed_pi ;
    }
}

return theta ;

}

请注意,修复的库实现很容易出错。您可以查看Optimizing Math-Intensive Applications with Fixed-Point Arithmetic。在正在讨论的库中使用C ++会使代码更加简单,在大多数情况下,您只需将floatdouble关键字替换为fixed即可。但它没有atan2()实现,上面的代码改编自我对该库的实现。