【问题标题】:Floating-point equality test and extra precision: can this code fail?浮点相等性测试和额外的精度:这段代码会失败吗?
【发布时间】:2013-04-25 20:55:21
【问题描述】:

讨论开始于my answer to another question。以下代码确定machine epsilon

float compute_eps() {
  float eps = 1.0f;

  while (1.0f + eps != 1.0f)
    eps /= 2.0f;

  return eps;
}

在 cmets 中提出 1.0f + eps != 1.0f 测试可能会失败,因为 C++ 标准允许使用额外的精度。虽然我知道浮点运算实际上是以更高的精度执行的(比使用的实际类型指定的),但我碰巧不同意这个提议。

我怀疑在比较操作期间,例如==!=,操作数没有被截断到它们类型的精度。换句话说,1.0f + eps当然可以用比float更高的精度来求值(例如long double),结果会存放在可以容纳long double的寄存器中。但是,我认为在执行!= 操作之前,左操作数将从long double 截断为float,因此代码永远不会无法准确地确定eps(即它永远不会进行比预期更多的迭代)。

我在 C++ 标准中没有找到任何关于这种特殊情况的线索。此外,代码运行良好,而且我确信在其执行期间使用了额外精度技术,因为我毫不怀疑任何现代桌面实现实际上在计算期间使用额外精度。

你怎么看?

【问题讨论】:

  • 在比较之前没有截断到类型的精度正是一些 GCC 用户在错误 323 的几个副本中抱怨的:gcc.gnu.org/bugzilla/show_bug.cgi?id=323(我不记得具体的错误报告,抱歉,错误 323 有点像包罗万象)。一个例子是double d = 3. / 7.; if (d != 3. / 7.) /* taken */ …
  • 为什么不能直接使用std::numeric_limits<T>::epsilon()
  • @masrtis:那是题外话。我可以,这里的重点不同。
  • 我没找过(可能来自C标准),但我记得规则是浮点计算可以以更高的精度完成,而 将结果值存储 到适当类型的变量中会降低存储类型的精度。即便如此,一些编译器仍然保持更高的精度,除非您使用命令行选项表示尊重浮点存储的规则。无论哪种方式,比较都不是存储,因此可以以更高的精度计算比较的一方或双方。

标签: c++ floating-point comparison precision equality


【解决方案1】:

很抱歉,这个例子是 C 而不是 C++。应该不难适应:

~ $ gcc -mfpmath=387 -mno-sse2  c.c
~ $ ./a.out 
incredible but true.
~ $ gcc -mfpmath=sse -msse2  c.c
~ $ ./a.out 
~ $ cat c.c
#include "stdio.h"

double d = 3. / 7.;
double d1 = 3.;

int main() {
  if (d != d1 / 7.)
    printf("incredible but true.\n");
  return 0;
}

gcc -msse2 -mfpmath=sse 是一个严格的 IEEE 754 编译器。使用该编译器,if 永远不会被使用。但是,gcc -mno-sse2 -mfpmath=387 必须使用精度更高的 387 单元。它不会降低!= 测试之前的精度。测试最终将右侧的 3. / 7. 的扩展精度结果与左侧相同除法的双精度结果进行比较。这会导致一种可能看起来很奇怪的行为。

gcc -msse2 -mfpmath=ssegcc -mno-sse2 -mfpmath=387 均符合标准。只是前者比较容易,生成 SSE2 指令,因此可以提供严格的 IEEE 754 实现,而后者必须在古老的指令集上尽力而为。

一个循环如:

while (eps1 != 1.0f)
  eps /= 2.0f, eps1 = 1.0f + eps;

eps1 声明为 float 类型在扩展精度方面应该更加健壮。


生成在比较之前不截断的x87代码的编译器是这个:

~ $ gcc -v
Using built-in specs.
Target: i686-apple-darwin11
Configured with: /private/var/tmp/llvmgcc42/llvmgcc42-2336.11~148/src/configure --disable-checking --enable-werror --prefix=/Applications/Xcode.app/Contents/Developer/usr/llvm-gcc-4.2 --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-prefix=llvm- --program-transform-name=/^[cg][^.-]*$/s/$/-4.2/ --with-slibdir=/usr/lib --build=i686-apple-darwin11 --enable-llvm=/private/var/tmp/llvmgcc42/llvmgcc42-2336.11~148/dst-llvmCore/Developer/usr/local --program-prefix=i686-apple-darwin11- --host=x86_64-apple-darwin11 --target=i686-apple-darwin11 --with-gxx-include-dir=/usr/include/c++/4.2.1
Thread model: posix
gcc version 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)

这是另一个:

~ $ clang -mno-sse2  c.c
~ $ ./a.out 
incredible but true.
~ $ clang -v
Apple LLVM version 4.2 (clang-425.0.24) (based on LLVM 3.2svn)
Target: x86_64-apple-darwin12.3.0
Thread model: posix

【讨论】:

  • 嗯,这是否意味着我的假设是正确的? IE。如果一切都符合标准,并且不存在细微的扩展或错误,那么操作数在比较之前会被截断?
  • @Haroogan 如果操作数在比较之前被截断,则消息“难以置信但真实”。永远不会显示。它显示了一些编译器,包括我在帖子中给出的特性。
  • 如果我们添加显式转换:float(1.0f + eps) != 1.0f 会怎样?这是否修正了问​​题?
  • @Haroogan 是的,演员表也应该消除多余的精度。请参阅 Joseph S. Myers 对实现应如何符合的分析:gcc.gnu.org/ml/gcc-patches/2008-11/msg00105.html
猜你喜欢
  • 2016-02-23
  • 2018-02-12
  • 2011-04-19
  • 1970-01-01
  • 1970-01-01
  • 2011-05-01
  • 2010-09-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多