此答案假定平台使用符合 IEEE-754 (2008) 的浮点运算并提供融合乘加 (FMA) 功能。 x86-64、ARM64 和 Power 等常见架构都满足这两个条件。 FMA 在 ISO C99 和更高版本的 C 标准中作为标准数学函数 fma() 公开。在不提供 FMA 指令的硬件上,这需要仿真,这可能会很慢而且functionally deficient。
在数学上,直角三角形中一条腿(导管)的长度,给定斜边和另一条腿的长度,简单地计算为√(h²-a²),其中h 是斜边的长度。但是当使用有限精度浮点算法进行计算时,我们面临两个问题:计算平方时可能会发生上溢或下溢为零,当平方具有相似幅度时,平方的减法会产生subtractive cancellation。
第一个问题很容易通过缩放 2n 来解决,这样幅度较大的项就会更接近统一。由于可能涉及次正规数,这不能通过操纵指数字段来完成,因为可能需要规范化/非规范化。但是我们可以通过指数字段位操作计算所需的比例因子,乘以因子。我们知道,对于非例外情况,斜边必须更长或与给定的边长度相同,因此可以根据该参数进行缩放。
处理减法消除比较困难,但我们很幸运,与我们的计算 h²-a² 非常相似的计算出现在其他重要问题中。比如浮点计算大师研究二次公式判别式的精确计算,b²-4ac:
William Kahan,“关于没有超精确算术的浮点计算的成本”,2004 年 11 月 21 日 (online)
最近,法国研究人员解决了两种产品差异的更一般情况,ad-bc:
Claude-Pierre Jeannerod、Nicolas Louvet、Jean-Michel Muller,“进一步分析 Kahan 的算法以准确计算 2 x 2 行列式。” 计算数学,卷。 82,284,2013年10月,第2245-2264页(online)
第二篇论文中基于 FMA 的算法计算两个产品的差异,经证明的最大误差为 1.5 ulp。有了这个构建块,我们就可以直接实现下面的导管计算的 ISO C99 实现。通过与任意精度库的结果进行比较确定,在 10 亿次随机试验中观察到的最大误差为 1.2 ulp:
#include <stdint.h>
#include <string.h>
#include <float.h>
#include <math.h>
uint64_t __double_as_uint64 (double a)
{
uint64_t r;
memcpy (&r, &a, sizeof r);
return r;
}
double __uint64_as_double (uint64_t a)
{
double r;
memcpy (&r, &a, sizeof r);
return r;
}
/*
diff_of_products() computes a*b-c*d with a maximum error < 1.5 ulp
Claude-Pierre Jeannerod, Nicolas Louvet, and Jean-Michel Muller,
"Further Analysis of Kahan's Algorithm for the Accurate Computation
of 2x2 Determinants". Mathematics of Computation, Vol. 82, No. 284,
Oct. 2013, pp. 2245-2264
*/
double diff_of_products (double a, double b, double c, double d)
{
double w = d * c;
double e = fma (-d, c, w);
double f = fma (a, b, -w);
return f + e;
}
/* compute sqrt (h*h - a*a) accurately, avoiding spurious overflow */
double my_cathetus (double h, double a)
{
double fh, fa, res, scale_in, scale_out, d, s;
uint64_t expo;
fh = fabs (h);
fa = fabs (a);
/* compute scale factors */
expo = __double_as_uint64 (fh) & 0xff80000000000000ULL;
scale_in = __uint64_as_double (0x7fc0000000000000ULL - expo);
scale_out = __uint64_as_double (expo + 0x0020000000000000ULL);
/* scale fh towards unity */
fh = fh * scale_in;
fa = fa * scale_in;
/* compute sqrt of difference of scaled arguments, avoiding overflow */
d = diff_of_products (fh, fh, fa, fa);
s = sqrt (d);
/* reverse previous scaling */
res = s * scale_out;
/* handle special arguments */
if (isnan (h) || isnan (a)) {
res = h + a;
}
return res;
}