恐怕我不知道如何保证正确舍入的双精度立方根,但可以提供一个非常接近正确舍入的值,如问题中所述。换句话说,最大误差似乎非常接近 0.5 ulp。
Peter Markstein,“IA-64 和基本函数:速度和精度”(Prentice-Hall 2000)
提出了有效的基于 FMA 的技术,用于正确舍入倒数、平方根和倒数平方根,但在这方面它没有涵盖立方根。一般来说,Markstein 的方法需要在最终舍入序列之前精确到 1 ulp以内的初步结果。我没有足够的数学能力将他的技术扩展到立方根的四舍五入,但在我看来,这在原则上应该是可能的,这是一个有点类似于平方根倒数的挑战。
按位算法很容易用正确的舍入计算根。由于 IEEE-754 舍入模式到最近或偶数的平局情况不会发生,因此只需进行计算,直到它产生所有尾数位加上一个舍入位。基于二项式定理的平方根的按位算法在非恢复和恢复变体中都是众所周知的,并且一直是硬件实现的基础。通过二项式定理的相同方法适用于立方根,并且有一篇鲜为人知的论文列出了非恢复实现的细节:
H. Peng,“提取平方根和立方根的算法”,第 5 届 IEEE 计算机算术国际研讨会论文集,第 121-126 页,1981 年。
最好我可以从试验中得知,这对于从整数中提取立方根来说效果很好。由于每次迭代只产生一个结果位,因此速度并不快。对于浮点运算中的应用,它的缺点是使用了几个簿记变量,这些变量需要大约两倍的最终结果的位数。这意味着需要使用 128 位整数运算来实现双精度立方根。
下面我的 C99 代码基于Halley 的有理法立方根,它具有三次收敛性,这意味着初始近似值不必非常准确,因为每次迭代中有效数字的数量都会增加三倍。可以以各种方式安排计算。通常,将迭代方案安排为在数值上是有利的
new_guess := old_guess + 更正
因为对于足够接近的初始猜测,correction
明显小于old_guess
. 这导致立方根的以下迭代方案:
x := x - x * (x 3 - a) / (2*x 3 + a)
这种特殊的排列方式也列在Kahan 关于立方根的笔记中。它的另一个优势是自然地使用FMA(融合乘加)。一个缺点是 2*x 3的计算可能导致溢出,因此至少部分双精度输入域需要参数缩减方案。在我的代码中,我只是将参数归约应用于所有非异常输入,基于对 IEEE-754 双精度操作数的指数的直接操作。
区间 [0.125,1) 用作主要近似区间。使用多项式极小极大近似,返回 [0.5,1] 中的初始猜测。窄范围便于对计算的低精度部分使用单精度算术。
我无法证明我的实现的错误范围,但是,针对参考实现(精确到大约 200 位)使用 2 亿个随机测试向量进行测试,发现总共有 277 个错误舍入的结果(因此错误率大约为 1.4 ppm)最大误差为 0.500012 ulps。
double my_cbrt (double a)
{
double b, u, v, r;
float bb, uu, vv;
int e, f, s;
if ((a == 0.0) || isinf(a) || isnan(a)) {
/* handle special cases */
r = a + a;
} else {
/* strip off sign-bit */
b = fabs (a);
/* compute exponent adjustments */
b = frexp (b, &e);
s = e - 3*342;
f = s / 3;
s = s - 3 * f;
f = f + 342;
/* map argument into the primary approximation interval [0.125,1) */
b = ldexp (b, s);
bb = (float)b;
/* approximate cube root in [0.125,1) with relative error 5.22e-3 */
uu = 0x1.2f32c0p-1f;
uu = fmaf (uu, bb, -0x1.62cc2ap+0f);
uu = fmaf (uu, bb, 0x1.7546e0p+0f);
uu = fmaf (uu, bb, 0x1.5d0590p-2f);
/* refine cube root using two Halley iterations w/ cubic convergence */
vv = uu * uu;
uu = fmaf (fmaf (vv, uu, -bb) / fmaf (vv, 2.0f*uu, bb), -uu, uu);
u = (double)uu;
v = u * u; // this product is exact
r = fma (fma (v, u, -b) / fma (v, 2.0*u, b), -u, u);
/* map back from primary approximation interval by jamming exponent */
r = ldexp (r, f);
/* restore sign bit */
r = copysign (r, a);
}
return r;
}