C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题

一个有趣的计算问题:世界首富马斯克,拥有10000亿人民币,假设都存在银行。银行存款月利息为3.33厘,银行偏偏不按月利息计算,而是按每纳秒计算其每一元利息,然后累加起来,马斯克每天可以获得的存款利息是多少呢?如果银行软件写的(故意),可能是0!

一、什么是计算精度问题?

1.1 类似于 马斯克银行存款的 利息问题

先看一句最简单的计算代码:

float a = 1.1111111e10F;
float b = 2.2222222e-12F;
float c = 3.3333333e-10F;
float d = a + b + c;
MessageBox.Show("d = " + String.Format("{0:F12}", d));

理论上 d = 11111111000.0000000003355555522

但不幸的是,d 最终却不是这个结果!!!!!!

C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题_第1张图片

为什么?因为计算精度(误差)的问题。

即使考虑到 优先度问题,写作:

float d = a + (b + c);

也无济于事。

如果我们换成精度更高的双精度数,如何?

double a = 1.1111111e+10;
double b = 2.2222222e-12;
double c = 3.3333333e-10;
double d = a + (b + c);
MessageBox.Show("d = " + String.Format("{0:F12}", d));
C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题_第2张图片

稍微好一点而已。

换做专门用于计算银行类数字(金融)的类型:

decimal a = 1.1111111e+10m;
decimal b = 2.2222222e-12m;
decimal c = 3.3333333e-10m;
decimal d = a + (b + c);
MessageBox.Show("d = " + String.Format("{0:F20}", d));

这下子好了!

C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题_第3张图片

那是不是都用 decimal 类型的就可以解决精度(误差)问题呢?

类型

精度

范围

占用位

decimal

28~29位有效

±1.0 × 10E−28 to ±7.9 × 10E28

128bit

double

15~16位有效

±5.0 × 10 E−324 to ±1.7 × 10E308

64bit

float

7位

±1.5 × 10E−45 to ±3.4 × 10E38

32bit

可见,不行!因为:

(1)decimal 关键字表示 128 位数据类型。 同浮点型相比,decimal 类型具有更高的精度和更小的范围,这使它适合于财务和货币计算。

(2)decimal的精度比double高,但数值范围比double小。decimal虽然是128位,但这只是因为它的小数位可以很多,但整体数值范围却没有double大。

(3)程序如果都用 128 位的数字来计算,计算速度 和 内存 开销都是无法承受的!

工业软件中,数值计算一般都以 float 为主。数据量不大的,用 double

计算精度问题是所有与计算相关的程序和程序员绕不过去的梦魇。

无论你水平多高,时间多长,多么地有经验,你都会时不时地被“精度“祸害一下。

1.2 来自微软(Microsoft Corp.)的总结与忠告

在许多情况下,浮点计算中的精度、舍入和准确性可以生成令程序员惊讶(出乎意外)的结果。他们应遵循四条一般规则:

  • 在涉及单精度和双精度的计算中,结果通常不会比单精度更准确。如果需要双精度,请确保计算中的所有项(包括常量)都以双精度定义。

  • 永远不要假设简单的数值在计算机中准确表示。大多数浮点值无法精确表示为有限的二进制值。例如,是二进制的(它永远重复),因此它不能在使用二进制算术(包括所有 PC)的计算机上完全准确地表示。.1.0001100110011...

  • 永远不要假设结果精确到小数点后一位。“正确”答案与任何浮点处理单元的有限精度计算得出的答案之间总是存在微小的差异。

  • 切勿比较两个浮点值以查看它们是否相等。这是规则3的必然结果。“应该”相等的数字之间几乎总是会有微小的差异。相反,请始终检查数字是否几乎相等。换句话说,检查它们之间的差异是否很小或微不足道。

1.3 误差与计算精度实例

误差是另外一个问题。

float a = 1.0f;
float b = 2.2222222e-12f;
float c1 = 3.3333333e-10f;
float c2 = 3.3333334e-10f;
float d = a + (b + c1);
float e = (a + b) + c2;
MessageBox.Show("d == e? " + ((d == e) ? "yes" : "no"));

大家预期的结果是什么呢?c1 != c2,应该是 no 。

实际是:

C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题_第4张图片

My God!

二、计算精度(误差)问题的严重性

2.1 数值溢出问题

1996 年,阿丽亚娜 5 型运载火箭首次飞行,搭载发射星群航天器(欧洲航天局的四大航天器之一的星座)。然而由于运载火箭无法到达指定轨道,任务以失败告终。

损失:3.7 亿美元,故障原因:阿丽亚娜5型运载火箭基于前一代4型火箭开发。在4型火箭系统中,对一个水平速率的测量值使用了16位的变量及内存,因为在4型火箭系统中反复验证过,这一值不会超过16位的变量,而5型火箭的开发人员简单复制了这部分程序,而没有对新火箭进行数值的验证,结果发生了致命的数值溢出。因此,飞行器在发射后 37 秒便从原始路径偏移。最终不得不启动了火箭自毁程序。

2.2 Intel的虫虫

1994 年,英特尔的奔腾微处理器芯片的浮点计算单元出现了一个 Bug。对于精确计算,处理器将返回不正确的十进制值。当时有大概 500 万个缺陷芯片在流通,英特尔最终决定为所有投诉的人更换芯片。这之后,英特尔把他们的故障处理器做成了钥匙链。

损失:4.75 亿美元 + 品牌名誉受损,故障原因:在奔腾浮点单元的分频器中有一个有缺陷的除法表,在约一千个条目中丢失了五条纪录。然而,这个错误在 90 亿随机浮点小数的除法中仅可能出现一次。例如,将 4195835.0 除以 3145727.0 得出 1.333739068902037589,而不是 1.333820449136241002,有 0.006% 的误差。

2.3 飞毛腿导弹炸死28名美军

1991 年 2 月第一次海湾战争期间,部署在沙特宰赫兰的美国爱国者导弹系统未能成功追踪和拦截来袭的伊拉克飞毛腿导弹。结果飞毛腿导弹击中美国军营。

损失:28 名士兵死亡,100 多人受伤,故障原因:时间计算不精确以及计算机算术错误导致了系统故障。从技术角度来讲,这是一个小的截断误差。当时,负责防卫该基地的爱国者反导弹系统已经连续工作了100个小时,每工作一个小时,系统内的时钟会有一个微小的毫秒级延迟,这就是这个失效悲剧的根源。爱国者反导弹系统的时钟寄存器设计为24位,因而时间的精度也只限于24位的精度。在长时间的工作后,这个微小的精度误差被渐渐放大。在工作了100小时后,系统时间的延迟是三分之一秒。

0.33 秒对常人来说微不足道。但是对一个需要跟踪并摧毁一枚空中飞弹的雷达系统来说,这是灾难性的。飞毛腿导弹空速达4.2马赫(每秒1.5公里),这个”微不足道的”0.33秒相当于大约 600 米的误差。在宰赫兰导弹事件中,雷达在空中发现了导弹,但由于时钟误差没能精确跟踪,反导导弹因而没有发射拦截。

C#,码海拾贝(01)——程序员不可不知的计算精度(误差)问题_第5张图片

三、简述计算精度问题的解决之道

实际上这是一个伪命题,如果有办法彻底解决精度与误差问题,至少可以获得图灵奖。

实际上,唯一有效的解决之道就是,怀疑任何计算结果!时刻保持警惕!

3.1 怎么保证得到期望的结果?

以下原则有利于尽量较少错误:

(1)加运算:大小差不多的数据先相加;

(2)差运算:将两个数放大 或 缩小 到差不多大小再计算,然后缩小回去;

(3)乘积:时刻小心是否会溢出!

(4)除法:时刻小心除数为0(或者接近0)!

(5)开平方:时刻小心,数据为负数!

在不在意计算效率,仅仅在意精度的情况下, BigDecimal 、BigInt、BigDouble 等等扩展的数据类型 类 class,可以解决问题。

3.2 怎么评判计算的结果?

避免对两个浮点数直接进行 等于比较!

double a = f1(x);
double b = f2(x);

// 错误的,不好的比较!
if(a == b) 
{
    ...
}

// 好的比较
if(Math.Abs(a - b) < float.Epsilon)
{
    ...
}

计算精度与误差问题是很难简单处理的难题。

你可能感兴趣的:(C#数值计算,Numerical,Recipes,C#入门教程,Beginner‘s,Recipes,c#,开发语言)