平方根、完全平方数和浮点数误差

9
在大多数编程语言中的sqrt函数(这里主要关注C和Haskell),它是否保证返回一个完全平方数的平方根是精确的?例如,如果我执行sqrt(81.0) == 9.0,这样安全吗?或者sqrt会返回8.999999998或9.00000003?
如果不能保证数值精度,那么如何检查一个数字是完全平方数呢?取平方根,获取最小整数和最大整数,确保它们的平方等于原始数字吗?
谢谢!

7
《计算机科学家应该了解的浮点运算知识》是一篇论文,讨论了计算机中用于表示浮点数的标准(IEEE 754)以及与其相关的精度、舍入误差、运算顺序等问题。本文旨在帮助读者了解这些问题,并提供一些实用的建议和技巧,以避免由于浮点运算产生的错误。 - Robert Harvey
7
众所周知,如果给出不精确的数字,浮点运算将产生不精确的结果。我认为问问题的人特别关注可以精确表示的精确数字,即使在浮点数中也是如此。他的问题更像是“如果f是一个精确的数字,那么特定的操作是否会产生精确的结果?” - Gabe
7
@Mitch: 9.0是一个整数,因此可以用二进制浮点数1.001*2^3来精确表示,并且由于其幅度小于所有IEEE格式的尾数,因此在任何格式中都可以精确表示(单精度表示为0x41100000,双精度表示为0x4022000000)。 - Gabe
1
如果某个数x.xxxx存在一个精确的平方根,它应该是y.yy的形式——将数字数量减半。 - Aki Suihkonen
7
迷信!平方根是基本的 IEEE 754 标准,必须正确舍入。请参考 tmyklebu 的回答。 - Pascal Cuoq
显示剩余3条评论
4个回答

12
在IEEE 754浮点数中,如果双精度值x是非负可表示数字y的平方(即y * y == x且计算y * y不涉及任何舍入,溢出或下溢),那么sqrt(x)将返回y。
这是因为IEEE 754标准要求sqrt正确舍入。也就是说,对于任何x,sqrt(x)都将是实际平方根最接近的双精度值。sqrt适用于完全平方数是这个事实的一个简单推论。
如果你想检查一个浮点数是否是完全平方数,这是我能想到的最简单的代码:
int issquare(double d) {
  if (signbit(d)) return false;
  feclearexcept(FE_INEXACT);
  double dd = sqrt(d);
  asm volatile("" : "+x"(dd));
  return !fetestexcept(FE_INEXACT);
}

我需要依赖于 dd 的空的 asm volatile 块,否则你的编译器可能会聪明地“优化掉”对 dd 的计算。

我使用了一些奇怪的来自 fenv.h 的函数,即 feclearexceptfetestexcept。最好查看它们的 man 页面。

另一个你可能能够让其工作的策略是计算平方根,检查低 26 位的尾数中是否有设置的比特,并在有时提出异议。我在下面尝试了这种方法。

我需要检查 d 是否为零,因为否则它可能会返回 true 用于 -0.0

编辑:Eric Postpischil 表示修改尾数可能更好。考虑到上面的 issquare 在另一个流行编译器 clang 中不起作用,我也同意这个看法。 我认为以下代码可以工作:

int _issquare2(double d) {
  if (signbit(d)) return 0;
  int foo;
  double s = sqrt(d);
  double a = frexp(s, &foo);
  frexp(d, &foo);
  if (foo & 1) {
    return (a + 33554432.0) - 33554432.0 == a && s*s == d;
  } else {
    return (a + 67108864.0) - 67108864.0 == a;
  }
}

a中加减67108864.0将擦除尾数的低26位。只有当这些位在一开始就被清除时,我们才能精确地得到a


2
在代码可以被称为“d是否为平方数”的实现之前,必须满足一些要求。您需要从C到IEEE 754或其他适当的浮点规范进行绑定,这是C标准不需要的,许多C实现也不提供。您必须包含<fenv.h>并使用适当的#pragma将FENV_ACCESS设置为on,或者知道正在使用的C实现已经打开了它。1/d会引发异常,可以通过使用signbit(d)来避免。当然,C实现必须支持asm扩展。 - Eric Postpischil
使用feclearexcept和fetestexcept访问浮点环境在某些处理器上可能非常缓慢。检查有效数字(而不是尾数)的低位将更快,因为这可以用普通算术完成。溢出、下溢和次正规数不会成为问题,因为平方根远离浮点范围的极端值。但是,我对所使用的精度有一些担忧。我不清楚C Annex F是否防止实现使用比标称类型更高的精度,就像没有Annex F那样允许的那样。 - Eric Postpischil
asm 是不是不必要的?在 double dd = sqrt(d); 之后有一个序列点,所以在调用 fetestexcept 之前,该操作的所有副作用应该已经完成了。 - Eric Postpischil
asm 块不是用于排序的;我试图告诉编译器 dd 并没有死亡,最好不要将其优化掉。无论是否执行了 #pragma STDC FENV_ACCESS ON。无论是 gcc 还是 clang,似乎都将 sqrt 视为没有副作用的纯函数。(我的 clang 版本甚至不在意依赖于 ddasm 块,并完全省略了它的计算。我猜,如果您想要一个可移植的实现,则确实需要进行位操作。叹气。) - tmyklebu
然而,这在测试67108863时失败了。平方根返回为0x1.ffffffcp+12,其低26位清除但不精确。因此,评估的sqrt(x)的低26位清除并不能保证平方根是精确的。 - Eric Postpischil
显示剩余4条评论

7
根据这篇论文,讨论了如何证明IEEE浮点数平方根的正确性:
IEEE-754二进制浮点算术标准[1]要求计算除法或平方根运算的结果,应该被视为无限精度计算,然后舍入到指定精度的两个最接近的浮点数之一。
由于在浮点数中可以精确表示的完全平方数是一个整数,其平方根也是可以精确表示的整数,因此完全平方数的平方根应始终准确无误。
当然,并不能保证您的代码将使用符合IEEE浮点库的执行。

假设sqrt例程的实现方式类似于长除法(重复移位和减法),该算法以零余数结束并产生精确结果。 - Aki Suihkonen

1

@tmyklebu已经完美回答了这个问题。作为补充,让我们看看一种可能不太高效的替代方法,用于测试分数的完全平方,而不使用asm指令。

假设我们有一个符合IEEE 754标准的sqrt函数,可以正确地四舍五入结果。
假设异常值(Inf/Nan)和零(+/-)已经处理好了。
sqrt(x)分解为I*2^m,其中I是奇整数。
并且I跨越n位:1+2^(n-1) <= I < 2^n

如果n > 1+floor(p/2),其中p是浮点精度(例如p=53,双精度中n>27)
那么2^(2n-2) < I^2 < 2^2n
由于I是奇数,I^2也是奇数,因此跨越了超过p位。
因此,I不是任何具有此精度的可表示浮点数的确切平方根。

但是,鉴于 I^2<2^p,我们能说 x 是一个完全平方数吗?
显然答案是否定的。泰勒展开式为

sqrt(I^2+e)=I*(1+e/2I - e^2/4I^2 + O(e^3/I^3))

因此,对于e = ulp(I ^ 2)直到sqrt(ulp(I ^ 2)),平方根被正确地舍入为rsqrt(I ^ 2 + e)= I...(四舍五入到最近的偶数或截断或向下模式)。

因此,我们必须断言sqrt(x)* sqrt(x)== x
但上述测试是不充分的,例如,假设IEEE 754双精度,sqrt(1.0e200)* sqrt(1.0e200)= 1.0e200,其中1.0e200恰好是99999999999999996973312221251036165947450327545502362648241750950346848435554075534196338404706251868027512415973882408182135734368278484639385041047239877871023591066789981811181813306167128854888448,其第一个质因子是2 ^ 613,几乎不是任何分数的完全平方数...

因此,我们可以结合两个测试:

#include <float.h>
bool is_perfect_square(double x) {
    return sqrt(x)*sqrt(x) == x
        && squared_significand_fits_in_precision(sqrt(x));
}
bool squared_significand_fits_in_precision(double x) {
    double scaled=scalb( x , DBL_MANT_DIG/2-ilogb(x));
    return scaled == floor(scaled)
        && (scalb(scaled,-1)==floor(scalb(scaled,-1)) /* scaled is even */
            || scaled < scalb( sqrt((double) FLT_RADIX) , DBL_MANT_DIG/2 + 1));
}

编辑: 如果我们想要限制在整数的情况下,我们也可以检查 floor(sqrt(x))==sqrt(x) 或者使用 squared_significand_fits_in_precision 的 dirty bit hacks...


该死。真希望我之前知道 ilogb - tmyklebu
不幸的是,尽管ilogb和scalb在好的库中可用,但它们直到C++11才被包含在C标准中...所以我有点作弊了。 - aka.nice

0

不要使用 sqrt(81.0) == 9.0,而是尝试使用 9.0*9.0 == 81.0。只要平方数在浮点数范围内,这种方法总是有效的。

编辑:我可能没有清楚地表达“浮点数范围”的含义。我的意思是将数字保持在整数值的范围内,而不会失去精度,即IEEE双精度的2 ** 53以下。我还预计会有单独的操作来确保平方根是一个整数。

double root = floor(sqrt(x) + 0.5);  /* rounded result to nearest integer */
if (root*root == x && x < 9007199254740992.0)
    /* it's a perfect square */

1
当您将无穷大替换为81.0和1e300替换为9.0时,它会失败(至少对于双精度浮点数)。或者保留81.0不变,使用-9.0替换9.0。 - tmyklebu
@EricPostpischil,我看到这个答案引起了一些混淆。希望我已经澄清了它。 - Mark Ransom
但是 scalb(FLT_RADIX, 2*DBL_MANT_DIG) 也是一个完美的平方数,不是吗? - aka.nice
的确不是,因为它是2的奇次幂。你可能想用scalbn(1.0,2*DBL_MANT_DIG)。 - Arne Vogel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接