为什么在 C# 中添加多个双精度数时顺序会影响舍入
考虑以下 C# 代码:
double result1 = 1.0 + 1.1 + 1.2;
double result2 = 1.2 + 1.0 + 1.1;
if (result1 == result2)
{
...
}
result1 应该始终等于 result2,对吗? 问题是,事实并非如此。 结果 1 为 3.3,结果 2 为 3.3000000000000003。 唯一的区别是常数的顺序。
我知道双打的实现方式可能会出现舍入问题。 我知道如果我需要绝对精度,我可以使用小数。 或者我可以在 if 语句中使用 Math.Round() 。 我只是一个想了解 C# 编译器在做什么的书呆子。 谁能告诉我吗?
编辑:
感谢迄今为止建议阅读浮点算术和/或谈论 CPU 处理双精度的固有不准确性的每个人。 但我觉得我的问题的主旨仍然没有得到解答。 这是我的错,因为措辞不正确。 让我这样说:
分解上面的代码,我希望发生以下操作:
double r1 = 1.1 + 1.2;
double r2 = 1.0 + r1
double r3 = 1.0 + 1.1
double r4 = 1.2 + r3
让我们假设上面的每个添加都有一个舍入错误(编号为 e1..e4)。 因此,r1 包含舍入误差 e1,r2 包含舍入误差 e1 + e2,r3 包含 e3,r4 包含 e3 + e4。
现在,我不知道舍入误差到底是如何发生的,但我预计 e1+e2 等于 e3+e4。 显然不是,但这对我来说似乎有些错误。 另一件事是,当我运行上面的代码时,我没有收到任何舍入错误。 这让我觉得是 C# 编译器而不是 CPU 在做一些奇怪的事情。
我知道我问了很多,也许任何人都能给出的最好答案就是去攻读 CPU 设计博士学位,但我只是想问一下。
编辑 2
查看我的原始代码示例中的 IL,很明显是编译器而不是 CPU 在执行此操作:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] float64 result1,
[1] float64 result2)
L_0000: nop
L_0001: ldc.r8 3.3
L_000a: stloc.0
L_000b: ldc.r8 3.3000000000000003
L_0014: stloc.1
L_0015: ret
}
编译器正在为我添加数字!
Consider the following C# code:
double result1 = 1.0 + 1.1 + 1.2;
double result2 = 1.2 + 1.0 + 1.1;
if (result1 == result2)
{
...
}
result1 should always equal result2 right? The thing is, it doesn't. result1 is 3.3 and result2 is 3.3000000000000003. The only difference is the order of the constants.
I know that doubles are implemented in such a way that rounding issues can occur. I'm aware that I can use decimals instead if I need absolute precision. Or that I can use Math.Round() in my if statement. I'm just a nerd who wants to understand what the C# compiler is doing. Can anyone tell me?
Edit:
Thanks to everyone who's so far suggested reading up on floating point arithmetic and/or talked about the inherent inaccuracy of how the CPU handles doubles. But I feel the main thrust of my question is still unanswered. Which is my fault for not phrasing it correctly. Let me put it like this:
Breaking down the above code, I would expect the following operations to be happening:
double r1 = 1.1 + 1.2;
double r2 = 1.0 + r1
double r3 = 1.0 + 1.1
double r4 = 1.2 + r3
Let's assume that each of the above additions had a rounding error (numbered e1..e4). So r1 contains rounding error e1, r2 includes rounding errors e1 + e2, r3 contains e3 and r4 contains e3 + e4.
Now, I don't know how exactly how the rounding errors happen but I would have expected e1+e2 to equal e3+e4. Clearly it doesn't, but that seems somehow wrong to me. Another thing is that when I run the above code, I don't get any rounding errors. That's what makes me think it's the C# compiler that's doing something weird rather than the CPU.
I know I'm asking a lot and maybe the best answer anyone can give is to go and do a PHD in CPU design, but I just thought I'd ask.
Edit 2
Looking at the IL from my original code sample, it's clear that it's the compiler not the CPU that's doing this:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] float64 result1,
[1] float64 result2)
L_0000: nop
L_0001: ldc.r8 3.3
L_000a: stloc.0
L_000b: ldc.r8 3.3000000000000003
L_0014: stloc.1
L_0015: ret
}
The compiler is adding up the numbers for me!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
并没有完全不同
这与期望相等
,只是在发言之前要乘以 2^53。
使用 12 位精度浮点并截断您的值:
因此,更改运算/截断的顺序会导致错误发生变化,并且 r4 != r2。 如果在此系统中添加 1.1 和 1.2,则最后一位会进位,因此在截断时不会丢失。 如果将 1.0 添加到 1.1,则 1.1 的最后一位会丢失,因此结果不一样。
在一种排序中,舍入(通过截断)会删除尾随的
1
。在另一种排序中,舍入两次都会删除尾随
0
。一不等于零; 所以错误并不相同。
双精度数具有更多位的精度,并且 C# 可能使用舍入而不是截断,但希望这个简单的模型向您展示相同值的不同排序可能会发生不同的错误。
fp 和 maths 之间的区别在于 + 是“add then round”的简写,而不仅仅是加。
That's not entirely unlike expecting
to equal
except you're multiplying by 2^53 before taking the floor.
Using 12 bit precision floating point and truncation with your values:
So changing the order of operations/truncations causes the the error to change, and r4 != r2. If you add 1.1 and 1.2 in this system, the last bit carries, so in not lost on truncation. If you add 1.0 to 1.1, the last bit of 1.1 is lost and so the result is not the same.
In one ordering, the rounding (by truncation) removes a trailing
1
.In the other ordering, the rounding removes a trailing
0
both times.One does not equal zero; so the errors are not the same.
Doubles have many more bits of precision, and C# probably uses rounding rather than truncation, but hopefully this simple model shows you different errors can happen with different orderings of the same values.
The difference between fp and maths is that + is shorthand for 'add then round' rather than just add.
C# 编译器没有执行任何操作。 CPU是。
如果 CPU 寄存器中有 A,然后添加 B,则存储在该寄存器中的结果是 A+B,近似于所使用的浮点精度
如果然后添加 C,则误差会累加。 这个错误加法不是传递操作,因此是最终的区别。
The c# compiler isn't doing anything. The CPU is.
if you have A in a CPU register, and you then add B, the result stored in that register is A+B, approximated to the floating precision used
If you then add C, the error adds up. This error addition is not a transitive operation, thus the final difference.
请参阅经典论文(每个计算机科学家都应该了解浮点运算)< /a> 关于这个主题。 这种情况就是浮点运算中发生的事情。 计算机科学家才能告诉您 1/3+1/3+1/3 不等于 1...
See the classic paper (What every computer scientist should know about floating-point arithmetic) on the subject. This kind of stuff is what happens with floating point arithmetic. It takes a computer scientist to tell you that 1/3+1/3+1/3 is'nt equal to 1...
浮点运算的顺序很重要。 不直接回答您的问题,但您应该始终小心比较浮点数。 通常包括容差:
这可能会令人感兴趣:每个计算机科学家应该做什么了解浮点运算
Order of floating point operations is important. Doesn't directly answer your question, but you should always be careful comparing floating point numbers. It's usual to include a tolerance:
This may be of interest: What Every Computer Scientist Should Know About Floating-Point Arithmetic
错误。 在数学中确实如此,但在浮点算术中却不然。
您需要阅读一些数值分析入门。
Wrong. That's true in mathematics, but no in floating-point arithmetics.
You'll need to read some Numerical Analysis primer.
为什么错误根据顺序不同可以用不同的示例来解释。
假设对于10以下的数字,它可以存储所有数字,因此它可以存储1、2、3等,直到10(包括10),但是在10之后,由于内部损耗,它只能存储每隔一个的数字精度,换句话说,它只能存储 10、12、14 等。
现在,通过该示例,您将了解为什么以下会产生不同的结果:
浮点数的问题在于它们无法表示准确,并且错误并不总是以相同的方式发生,因此顺序很重要。
例如,3.00000000003 + 3.00000000003 最终可能是 6.00000000005(注意最后不是 6),但 3.00000000003 + 2.99999999997 最终可能是 6.00000000001,这样:
但是,改变顺序:
很重要。
当然,现在您可能很幸运,因为上面的示例相互平衡,第一个将向上摆动 .xxx1,另一个将向下摆动 .xxx1,从而为您提供两个 .xxx3,但不能保证。
Why the errors aren't the same depending on order can be explained with a different example.
Let's say that for numbers below 10, it can store all the numbers, so it can store 1, 2, 3, and so on up to and including 10, but after 10, it can only store every second number, due to internal loss of precision, in other words, it can only store 10, 12, 14, etc.
Now, with that example, you'll see why the following produces different results:
The problem with floating point numbers is that they can't be represented accurately, and the error doesn't always go the same way, so the order will matter.
For instance, 3.00000000003 + 3.00000000003 might end up being 6.00000000005 (notice not 6 at the end), but 3.00000000003 + 2.99999999997 might end up being 6.00000000001, and with that:
but, change the order:
So it will matter.
Now, of course, you might be lucky in that the above examples balance each other out, in that the first will swing up by .xxx1 and the other down by .xxx1, giving you .xxx3 in both, but there's no guarantee.
实际上,您没有使用相同的值,因为中间结果不同:
因为双精度数不能准确地表示十进制值,所以您会得到不同的结果。
You are actually not using the same values because the intermediate results are different:
Because doubles can't represent decimal values exactly you get different results.