最近我在阅读Eric Lippert的旧博客文章时,他在写关于结合律时提到,在C#中,对于某些a、b和c的值,(a + b) + c
并不等同于a + (b + c)
。
我无法确定这是否适用于哪些类型和算术值的范围,以及为什么会这样。
最近我在阅读Eric Lippert的旧博客文章时,他在写关于结合律时提到,在C#中,对于某些a、b和c的值,(a + b) + c
并不等同于a + (b + c)
。
我无法确定这是否适用于哪些类型和算术值的范围,以及为什么会这样。
在 double
类型的范围内:
double dbl1 = (double.MinValue + double.MaxValue) + double.MaxValue;
double dbl2 = double.MinValue + (double.MaxValue + double.MaxValue);
第一个是double.MaxValue
,第二个是double.Infinity
关于double
类型的精度:
double dbl1 = (double.MinValue + double.MaxValue) + double.Epsilon;
double dbl2 = double.MinValue + (double.MaxValue + double.Epsilon);
现在dbl1 == double.Epsilon
,而dbl2 == 0
。
字面上读这个问题 :-)
在checked
模式下:
checked
{
int i1 = (int.MinValue + int.MaxValue) + int.MaxValue;
}
i1
是 int.MaxValue
checked
{
int temp = int.MaxValue;
int i2 = int.MinValue + (temp + temp);
}
(请注意使用temp
变量,否则编译器会直接报错...从技术上讲,这甚至会产生不同的结果 :-) 编译正确与无法编译)
这会抛出一个OverflowException
...结果是不同的 :-) (int.MaxValue
vs Exception
)
一个例子
a = 1e-30
b = 1e+30
c = -1e+30
(0.1+0.2)+0.3
和0.1+(0.2+0.3)
。详情请见http://www.walkingrandomly.com/?p=5380。 - phuclv在其他回答中已经展示了极小和极大数字时会得到不同的结果,这里提供一个例子,使用实际正常数字的浮点数也会得到不同的答案。
在这种情况下,我没有使用精度极限的数字,而是进行了很多加法操作。区别在于使用 (((...(((a+b)+c)+d)+e)...
或者 ...(((a+b)+(c+d))+((e+f)+(g+h)))+...
我在这里使用的是Python,但如果您在C#中编写此代码,则可能会得到相同的结果。首先创建一个包含一百万个值的列表,所有值均为0.1。从左侧开始将它们相加,您会看到舍入误差变得显著:
>>> numbers = [0.1]*1000000
>>> sum(numbers)
100000.00000133288
>>> def pair_sum(numbers):
if len(numbers)==1:
return numbers[0]
if len(numbers)%2:
numbers.append(0)
return pair_sum([a+b for a,b in zip(numbers[::2], numbers[1::2])])
>>> pair_sum(numbers)
100000.0
pair_sum()
相同的答案。def pair_sum(seq):
tmp = []
for i,v in enumerate(seq):
if i&1:
tmp[-1] = tmp[-1] + v
i = i + 1
n = i & -i
while n > 2:
t = tmp.pop(-1)
tmp[-1] = tmp[-1] + t
n >>= 1
else:
tmp.append(v)
while len(tmp) > 1:
t = tmp.pop(-1)
tmp[-1] = tmp[-1] + t
return tmp[0]
这里是用C#编写的简单pair_sum代码:
using System;
using System.Linq;
namespace ConsoleApplication1
{
class Program
{
static double pair_sum(double[] numbers)
{
if (numbers.Length==1)
{
return numbers[0];
}
var new_numbers = new double[(numbers.Length + 1) / 2];
for (var i = 0; i < numbers.Length - 1; i += 2) {
new_numbers[i / 2] = numbers[i] + numbers[i + 1];
}
if (numbers.Length%2 != 0)
{
new_numbers[new_numbers.Length - 1] = numbers[numbers.Length-1];
}
return pair_sum(new_numbers);
}
static void Main(string[] args)
{
var numbers = new double[1000000];
for (var i = 0; i < numbers.Length; i++) numbers[i] = 0.1;
Console.WriteLine(numbers.Sum());
Console.WriteLine(pair_sum(numbers));
}
}
}
输出结果:
100000.000001333
100000
几个类似的例子:
static void A(string s, int i, int j)
{
var test1 = (s + i) + j;
var test2 = s + (i + j);
var testX = s + i + j;
}
这里的 A("Hello", 3, 5)
导致 test1
和 testX
等于 "Hello35"
,而 test2
将会是 "Hello8"
。
另外:
static void B(int i, int j, long k)
{
var test1 = (i + j) + k;
var test2 = i + (j + k);
var testX = i + j + k;
}
在通常的unchecked
模式下,B(2000000000, 2000000000, 42L)
导致test1
和testX
等于-294967254L
,而test2
变成了4000000042L
。
简短回答是在数学上(a+b)+c=a+(b+c)
,但在计算上不一定成立。
请记住,电脑实际工作的是二进制,甚至一个简单的小数在转换为内部格式时也会导致舍入误差。
根据编程语言的不同,即使加法也可能会产生舍入误差,在上面的示例中,a+b
中的舍入误差可能与b+c
中的误差不同。
令人惊讶的是,JavaScript 就是这个罪魁祸首:0.1+0.2!=0.3
。这种舍入误差虽然在小数点后很长时间才出现,但确实存在并且带来问题。
通常我们可以通过先添加小数点后面的小数来减少舍入误差。这样它们就可以在被大数淹没之前累积。