C和C#之间的浮点精度行为差异

问题描述

这是一个学术性的问题,因此诸如“只是不要那样做”之类的答案是没有道理的。

我不是要解决问题,而是要了解观察到的行为,即在比较C和C#时浮点数学运算如何起作用的差异

假设:C语言中的浮点精度

我假设在C type Dogs = {bark: true}; type Cats = {meow: true}; const data: [Dogs,Cats][] = [[{bark: true},{meow: true}]] const display = (dogs: Dogs,cats: Cats) => { return 'dummy' } let result: {dogs: Dogs[],cats: Cats[]} = {dogs: [],cats: []}; data.reduce((acc,curr) => { const [dog,cat] = curr; acc.dogs.push(dog); acc.cats.push(cat); return acc },result); 中使用23位尾数和8位指数(https://en.wikipedia.org/wiki/Single-precision_floating-point_format)来实现

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

如果浮点数计算为:

floats

然后,因为尾数有23位,所以精度的值为[sign] * 1.[mantissa] * 2^[exponent] ,其中给定数字的指数为:

2^(exponent-23)

因此,像floor(log2(number)) 这样的相当大的数字的精度计算如下:

10^9

这是绝对值,它是绝对值,它在以浮点数存储时可以添加exponent = floor(log2(10^9)) = 29 precision = 2^(exponent-23) = 2^(29-23) = 2^6 = 64 上,这是绝对的最低值,因为我们实际上是在翻转尾数的最低有效位:

enter image description here

As visualized by the IEEE-754 Floating Point Converter

我还可以使用快速的C程序(run online)对此进行验证:

10^9

我以为一般的32位浮点格式(1位符号,8位指数,23位尾数)是如此普遍,以至于它是现代cpu固有的东西,因此通常行为在所有编程语言中都是相同的

问题:C#中的浮点精度

因此,我在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; } ,则它的指数为10^8,因此精度为26,考虑到以上关于浮点格式的位表示方式的假设内部数字,我注意到以下行为:

2^(26-23) = 8

那……使我有些困惑。那一百个从哪里来?那甚至不是2的倍数!

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

看着C# documentation for floating point values,没有什么让我惊讶的,这表明C#应该在这里处理与C有所不同的浮点加法,并且考虑到如何实现浮点值,我期望得到什么。

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

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

解决方法

将我的评论升级为答案:

这里的问题不是浮点数,而是字符串格式的差异。我不确切地知道指定为“ 0”的格式的含义或含义(并且似乎找不到在任何地方记录的格式),但是它是造成您所看到的异常舍入的原因。

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