首页 > 解决方案 > C 和 C# 之间浮点精度行为的差异

问题描述

这是一个学术问题,因此诸如“不要那样做”之类的答案没有抓住重点。

我不是在尝试解决问题 - 我是在尝试理解观察到的行为,即比较 C 和 C# 时浮点数学的功能差异

假设:C 中的浮点精度

我的假设是在 Cfloats中使用 23 位尾数和 8 位指数(https://en.wikipedia.org/wiki/Single-precision_floating-point_format)实现

对于给定的数字,我们可以通过计算尾数的最后一位的值来计算最小精度 - 您可以添加到纯粹结构上不能再存储它的数字的最小值。

如果浮点数被评估为:

[sign] * 1.[mantissa] * 2^[exponent]

然后因为我们在尾数中有 23 位精度的值是2^(exponent-23),其中给定数字的指数是:

floor(log2(number))

所以一个相当大的数字的精度10^9如下计算:

exponent  = floor(log2(10^9))
          = 29

precision = 2^(exponent-23)
          = 2^(29-23)
          = 2^6
          = 64

这是在存储为浮点数时可以添加的裸机、理论上可能的最低值10^9,因为我们实际上是在翻转尾数的最低有效位: 正如 IEEE-754 浮点转换器所显示的那样在此处输入图像描述

我还可以使用快速 C 程序(在线运行)来验证这一点:

#include <cstdio>

int main()
{  
  float number = 1e9f;          // exponent: 29, precision: 64
  printf("%'.0f\n", number);    // prints: 1000000000 
  
  number += 30;                 // 30 rounded to nearest multiple of 64 is 0 
  printf("%'.0f\n", number);    // prints: 1000000000 
  
  number += 40;                 // 40 rounded to nearest multiple of 64 is 64
  printf("%0'.0f\n", number);   // prints: 1000000064 
  
  return 0;
}

我的假设是,通用的 32 位浮点格式(1 位符号、8 位指数、23 位尾数)是如此普遍,以至于它是现代 CPU 固有的东西,因此通常跨编程语言的行为是相同的。

问题:C#中的浮点精度

因此,当我在 C# 中尝试相同的验证测试时,数字的值不会改变。

如果我使用较小的 value 10^8,它的指数是26,因此精度是 ,2^(26-23) = 8我上面假设浮点格式的位如何在内部表示数字,我注意到以下行为:

float number = 1e8f;                 // exponent: 26, precision: 8
Console.WriteLine($"{number,1:0}");  // prints: 100000000 

number += 30;                        // 30 rounded to multiple of 8 -should- be 32
Console.WriteLine($"{number,1:0}");  // prints: 100000000 

number += 40;                        // 40 rounded to multiple of 8 -should- be 40
Console.WriteLine($"{number,1:0}");  // prints: 100000100

这……让我有些困惑。那100从哪里来?这甚至不是2的倍数!

值为 1e8f C 的行为也符合预期并支持精度为“8”:cpp.sh/6qesv

查看浮点值的C# 文档没有什么让我觉得 C# 应该以与 C 不同的方式处理浮点加法,以及考虑到浮点值的实现方式,我所期望的。

文档确实提到浮点数的近似精度约为 6-9 位,这令人沮丧地模糊。我想这可能是一个答案:“您正在处理超出保证限制的数字,这是未定义的行为”,虽然这是真的,但令人不满意。

我想知道,理想情况下逐步分解,在那里的 C# 实现中实际发生了什么,使它的行为与这里的 C 如此不同。

标签: c#cfloating-pointprecisionieee-754

解决方案


将我的评论推广到答案:

这里的问题不是浮点数,而是字符串格式的差异。我不熟悉指定为“0”的格式究竟是什么意思或作用(似乎在任何地方都找不到它的记录),但它是造成您看到的不寻常的舍入的原因。

建议使用“G9”的格式说明符来格式化单精度浮点数,以使其正确往返(这意味着将字符串解析回单精度浮点数将准确再现原始值)。如果您更改代码以{number:G9}在插值字符串中使用,您应该会看到预期的结果。


推荐阅读