【问题标题】:Comparing floating point values converted from strings with literals比较从字符串转换的浮点值与文字
【发布时间】:2018-03-30 08:17:05
【问题描述】:

这不是著名的Is floating point math broken 的复制品,即使它乍一看很像。

我正在使用fscanf(file, "%lf", &value); 从文本文件中读取double,并将其与== 运算符与双字面值进行比较。如果字符串与文字相同,那么使用== 的比较是否在所有情况下都是true

示例

文本文件内容:

7.7

代码sn-p:

double value;
fscanf(file, "%lf", &value);     // reading "7.7" from file into value

if (value == 7.7)
   printf("strictly equal\n");

预期和实际输出是

strictly equal

但这假设编译器将双精度字面值7.7 转换为双精度值,其方式与 fscanf 函数完全相同,但编译器可能使用也可能不使用相同的库将字符串转换为双精度值。

或者另外问:从字符串到双精度的转换是否会产生唯一的二进制表示,或者可能存在轻微的实现相关差异?

Live demonstration

【问题讨论】:

标签: c++ c floating-point language-lawyer


【解决方案1】:

来自 c++ 标准:

[lex.fcon]

...如果缩放值在范围内 其类型的可表示值,如果可表示,则结果是缩放值,否则更大或更小 最接近缩放值的可表示值,以实现定义的方式选择...

强调我的。

因此,只有当值可以用 double 严格表示时,您才能依赖相等性。

【讨论】:

  • @YSC 我很惊讶 cppreference 提到它。该网站每天都在改进。
  • 您实际上可以加入我们的工作:cppreference is a wiki!
  • @RichardHodges [lex.fcon] 是什么?
  • 这听起来确实是最安全的假设。我的经验是,这样的事情很容易被烫到。一方面,我不确定我对fscanf 所做的转换与编译器在编译时所做的转换相匹配的程度有多大。另一个是值有时会出现在寄存器中,其精度比人们预期的要高。但如果表示是精确的双精度,那么它似乎应该是安全的。
  • @MichaelWalz 这是 c++ 标准中的一个部分的名称。
【解决方案2】:

关于 C++,from cppreference one can read:

[lex.fcon](第 6.4.4.2 节)

评估浮点常量的结果是最接近的可表示值,或者是紧邻最近的可表示值的较大或较小的可表示值,以实现定义的方式选择(换句话说,转换期间的默认舍入方向是实现-定义)。

由于未指定浮动文字的表示,我猜你无法得出关于它与 scanf 结果比较的结论。


关于 C11(标准 ISO/IEC 9899:2011):

[lex.fcon](第 6.4.4.2 节)

推荐做法

7 浮点常量的翻译时转换应与库函数对字符串的执行时转换相匹配,例如strtod,给定适合两种转换的匹配输入、相同的结果格式和默认执行时间 四舍五入。

很明显,对于 C11,这不能保证匹配。

【讨论】:

  • 这听起来很有说服力。所以我会用对一些AmostEqual函数的调用替换==
  • @MichaelWalz> 你标记了 C 和 C++。我想知道他们是否同意这一点。在C99,规范性附录 F,第 7.2 小节写道:“在翻译过程中,IEC 60559 默认模式生效:- 舍入方向模式是舍入到最接近的 [...]”。因此,只要您不更改代码的浮点环境,就可以保证您的示例代码在 C99 中工作。
  • 所以,@YSC,您关于 C11 的观点是,翻译时间和运行时转换是否一致只是建议,而不是要求?
  • @YSC> 建议是关于它们应该匹配的事实。但是,规范的附件 F 给出了翻译时间的确切规则,因此通过明确配置浮点环境,应该保证匹配。
  • @Michael 在 x86 上这可能会失败的一个简单原因是,从内存中读取值只会获得 64 位精度,而在寄存器中的值可能具有 80 位精度。加载常量很可能直接通过寄存器完成,而 fscanf 加载的值可能已经存储在内存中的某个地方。仅仅因为这个原因,假设浮点数之间的相等几乎是不安全的。
【解决方案3】:

如果字符串与文字相同,那么使用== 进行的比较是否在所有情况下都为真?

尚未探索的常见注意事项:FLT_EVAL_METHOD

#include <float.h>
...
printf("%d\n", FLT_EVAL_METHOD);

2 评估所有操作和常量的范围和精度 long double 类型。

如果返回 2,则 value == 7.7 中使用的数学运算为 long double7.7 被视为 7.7L。在 OP 的情况下,这可能评估为 false。

要考虑到这种更广泛的精度,请分配将删除所有额外范围和精度的值。

scanf(file, "%lf", &value);
double seven_seven = 7.7;
if (value == seven_seven)
  printf("strictly equal\n");

IMO,这比变体舍入模式或库/编译器转换中的变体更容易发生。


请注意,此案例类似于以下众所周知的问题。

float value;
fscanf(file, "%f", &value);
if (value == 7.7)
   printf("strictly equal\n");

示范

#include <stdio.h>
#include <float.h>
int main() {
  printf("%d\n", FLT_EVAL_METHOD);
  double value;
  sscanf("7.7", "%lf", &value);
  double seven_seven = 7.7;
  if (value == seven_seven) {
    printf("value == seven_seven\n");
  } else {
    printf("value != seven_seven\n");
  }
  if (value == 7.7) {
    printf("value == 7.7\n");
  } else {
    printf("value != 7.7\n");
  }
  return 0;
}

输出

2
value == seven_seven
value != 7.7

替代比较

为了比较两个“接近”的double,我们需要一个“接近”的定义。一种有用的方法是考虑将所有有限的double 值排序为升序,然后将它们的序列号相互比较。 double_distance(x, nextafter(x, 2*x) --> 1

以下代码对double 布局和大小进行了各种假设。

#include <assert.h>

unsigned long long double_order(double x) {
  union {
    double d;
    unsigned long long ull;
  } u;
  assert(sizeof(double) == sizeof(unsigned long long));
  u.d = x;
  if (u.ull & 0x8000000000000000) {
    u.ull ^= 0x8000000000000000;
    return 0x8000000000000000 - u.ull;
  }
  return u.ull + 0x8000000000000000;
}

unsigned long long double_distance(double x, double y) {
  unsigned long long ullx = double_order(x);
  unsigned long long ully = double_order(y);
  if (x > y) return ullx - ully;
  return ully - ullx;
}

....
printf("%llu\n", double_distance(value, 7.7));                       // 0
printf("%llu\n", double_distance(value, nextafter(value,value*2)));  // 1
printf("%llu\n", double_distance(value, nextafter(value,value/2)));  // 1

或者直接使用

if (nextafter(7.7, -INF) <= value && value <= nextafter(7.7, +INF)) {
  puts("Close enough");
}

【讨论】:

  • 我几乎赞成,但union 类型双关语打破了 C++ 别名限制(这也是一个反复出现的话题)。一个格式良好的解决方案将例如使用memcpy
  • @ArneVogel 关于 C++ 说得够好。然而,帖子也被标记为 C,这个答案不会打破别名限制。鉴于fscanf(),OP 代码闻起来更像 C 而不是 C++。
【解决方案4】:

没有保证。

您可以希望编译器使用高质量的算法来转换文字,并且标准库实现也使用高质量的转换,并且两个高质量的算法应该经常一致。

也有可能两者都使用完全相同的算法(例如,编译器通过将字符放入 char 数组并调用 sscanf 来转换文字。

顺便说一句。我遇到了一个错误,原因是编译器没有完全转换文字 999999999.5。用 9999999995 / 10.0 替换它,一切都很好。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-12
    • 2011-11-25
    • 2013-08-06
    • 2022-08-03
    相关资源
    最近更新 更多