关联性数学:(a + b) + c != a + (b + c)
Associativity math: (a + b) + c != a + (b + c)
最近我正在经历一个 old blog post by Eric Lippert
其中,在撰写有关关联性的文章时,他提到在 C# 中,对于 a、b、c 的某些值,(a + b) + c
不等同于 a + (b + c)
。
我无法弄清楚什么类型和范围的算术值可能成立以及为什么。
一个例子
a = 1e-30
b = 1e+30
c = -1e+30
在 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
变量,否则编译器会直接报错...从技术上讲,即使这将是不同的结果:-)正确编译 vs 不编译)
这会引发 OverflowException
... 结果不同 :-)(int.MaxValue
vs Exception
)
扩展其他答案,这些答案显示了小数和大数的极端情况如何得到不同的结果,这是一个示例,其中具有实际正态数的浮点数给您不同的答案。
在这种情况下,我没有使用精度极限的数字,而是简单地做了很多加法。区别在于 (((...(((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
这源于普通值类型(int、long 等)使用固定数量的字节存储的事实。因此,当两个值的总和超过字节存储容量时,可能会发生溢出。
在C#中,可以使用BigInteger来避免这种问题。 BigInteger 的大小是任意的,因此不会产生溢出。
BigInteger 仅适用于 .NET 4.0 及更高版本 (VS 2010+)。
简短的回答是 (a + b) + c == a + (b + c)
数学上的,但不一定是计算上的。
记住计算机确实以二进制工作,即使是简单的小数在转换为内部格式时也会导致舍入错误。
根据语言的不同,即使加法也会产生舍入误差,在上面的示例中,a+b
中的舍入误差可能与 b+c
中的舍入误差不同。
一个令人惊讶的违规者是 JavaScript:0.1 + 0.2 != 0.3
。舍入误差在小数点后有很长的距离,但真实且有问题。
通过先添加小部分来减少舍入误差是一般原则。这样他们就可以在被更大的数字淹没之前积累起来。
几个类似的例子:
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;
}
这里B(2000000000, 2000000000, 42L)
导致test1
和testX
在通常的unchecked
模式下等于-294967254L
,而test2
变成4000000042L
.
在同一博客的评论中我发现了这个,C# 中的“^”符号是什么意思
a = 10^x
b = -a
c = 5
(a+b) + c == 5
a + (b+c) == 0
最近我正在经历一个 old blog post by Eric Lippert
其中,在撰写有关关联性的文章时,他提到在 C# 中,对于 a、b、c 的某些值,(a + b) + c
不等同于 a + (b + c)
。
我无法弄清楚什么类型和范围的算术值可能成立以及为什么。
一个例子
a = 1e-30
b = 1e+30
c = -1e+30
在 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
变量,否则编译器会直接报错...从技术上讲,即使这将是不同的结果:-)正确编译 vs 不编译)
这会引发 OverflowException
... 结果不同 :-)(int.MaxValue
vs Exception
)
扩展其他答案,这些答案显示了小数和大数的极端情况如何得到不同的结果,这是一个示例,其中具有实际正态数的浮点数给您不同的答案。
在这种情况下,我没有使用精度极限的数字,而是简单地做了很多加法。区别在于 (((...(((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
这源于普通值类型(int、long 等)使用固定数量的字节存储的事实。因此,当两个值的总和超过字节存储容量时,可能会发生溢出。
在C#中,可以使用BigInteger来避免这种问题。 BigInteger 的大小是任意的,因此不会产生溢出。
BigInteger 仅适用于 .NET 4.0 及更高版本 (VS 2010+)。
简短的回答是 (a + b) + c == a + (b + c)
数学上的,但不一定是计算上的。
记住计算机确实以二进制工作,即使是简单的小数在转换为内部格式时也会导致舍入错误。
根据语言的不同,即使加法也会产生舍入误差,在上面的示例中,a+b
中的舍入误差可能与 b+c
中的舍入误差不同。
一个令人惊讶的违规者是 JavaScript:0.1 + 0.2 != 0.3
。舍入误差在小数点后有很长的距离,但真实且有问题。
通过先添加小部分来减少舍入误差是一般原则。这样他们就可以在被更大的数字淹没之前积累起来。
几个类似的例子:
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;
}
这里B(2000000000, 2000000000, 42L)
导致test1
和testX
在通常的unchecked
模式下等于-294967254L
,而test2
变成4000000042L
.
在同一博客的评论中我发现了这个,C# 中的“^”符号是什么意思
a = 10^x
b = -a
c = 5
(a+b) + c == 5
a + (b+c) == 0