我做了一个计算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;
}
答案 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所属的段最有效的系数。
#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 ++会使代码更加简单,在大多数情况下,您只需将float
或double
关键字替换为fixed
即可。但它没有atan2()
实现,上面的代码改编自我对该库的实现。