一起谈.NET技术,.NET 中的二进制浮点类型

  大多数人会对他们在.NET中的算术的"出错"首先感到惊讶。使用一些称为”浮点”算术来表示非整型数字不是.NET 相比其他大多数语言/平台特殊的地方。在.NET 内部是没问题的,但是你需要知道一些底层正在发生什么,否则你将会对一些结果感到惊讶。

  我在这个事情上不是一个专家这不重要。虽然写了这篇文章,我也发现了另外一篇 - 这次是一个真正的专家写的,杰弗里 萨克斯(Jeffrey Sax)。我强烈建议你也同时读他的浮点文章

  什么是浮点数?

  计算机总是需要一些表示数据的方式,最终这些表示数据的方式总是归结为二进制(0,1组合)。整数很容易表示(对负数有合适的转换,有确定好的范围可以知道表示从多大开始)但是非整数有一些复杂。不管你想出什么方法,总是有一个问题。例如,使用我们自己的十进制方式写数字: 仍然(在十进制内部)不能表达三分之一,你只是在一个3循环中结束。无论你使用多少进制,一些数字都会产生同样的问题 - 特别的,“无理数”的数字(那些不能用以分数表示的数字)如常量PI(音: pai)和e(指数e)总是有一些问题。

  你可以将所有有理数用精确的两个整型数表示,第一个数被第二个数除的结果 - 但是即便是一个非常”简单”的操作整数都可以增长的非常大且非常快,平方根操作也会趋向产生无理数。有很多其他的因素会导致导致,但是最常用的解决问题的方式就是使用一种格式或其他格式的浮点类型。思想就是基础有可以用来扩展表达的一些数字(尾数),另外(指数)用来表示规模是多大,以“小数点要去哪里”的形式表示。例如,34.5可以用”十进制浮点类型”3.45加上一个指数1来表示,同样的3450也可以有同样的尾数和一个指数3(34.5是3.45x101,3450是 3.45x103)来表示。现在,为了简单起见例子使用十进制表示,但是大多数浮点类型是二进制表示的。例如,二进制尾数1.1加上尾数-1将意味着十进制0.75(二进制1.1==十进制1.5,在二进制中指数-1意味着”被2除”,十进制同样的指数-1表示”被10除”,二进制1.1==20.2-1==1.5(译者注)).

  理解在同样的方式你不能通过一个十进制扩充(无限)来精确表达三分之一是很重要的,有很多数字在十进制形式看起来很简单,但是在二进制表示中却有长的或者无限的扩展。这意味着(举例)一个二进制浮点变量不能有精确的十进制值0.1。相反,假设你又一些如下代码:

  double x = 0.1d;

  变量x实际上将存储最接近那个值的double型值。一旦你脑子里可以转过弯儿,那么为什么一起计算结果看起来是”错误”的将会变得很明显。如果你被要求计算1/3 + 1/3,这两个数相加的结果是0.666,而不是0.667(更接近两个1/3 的和)。一个二进制浮点类型的表达式是3.65d+0.05d != 3.7d(尽管在一些情况下它显示成3.7)

  .NET 中的浮点类型是什么样子的?

  C#标准仅列出double和float作为可用的浮点类型(这些是C#中System.Double和System.Single的速记表示),但是decimal类型(速记表示为System.Decimal)实际上也是一个浮点类型 - 它仅是十进制浮点类型,但是指数的范围很有趣。decimal类型在另外一篇文章中描述,所以这篇文章不会做任何深入探讨 - 我们关注double和float.这两个都是二进制浮点类型,参照IEEE 754(一个多种浮点类型的标准定义)。float是一个32位类型(1个符号位, 23位的尾数和8位指数), double是一个64位类型(1个符号位, 52位尾数和11位指数)。

  结果不是我期望的是不好的结果吗?

  好吧,那取决于情况。如果你在写财务软件,你可能要非常严格的定义处理错误的方式,数量也是直觉上用10进制表示 - 在这种情况decimal类型更加与float或者double类型相似。如果,然而,如果你在写一个科学应用程序,使用十进制浮点表示法可能会有一点弱,你也可能想要开始处理一些低精度的数目(一美元就是一美元,但是如果你在测量一个单位是米的长度,你可能开始有一些不精确。)

  比较浮点数字

  所有这些可以得出一个推论,你应该非常,非常少的去直接比较浮点数间是否相等。通常比较大于或者小于会好些,但是当你对相等感兴趣时你应该总是考虑是否你实际上想要的接近相等:一个数字总是与另外一个相同。做这个的一个简单的方式是用一个数减去另外一个数,使用Math.Abs来找到绝对值的不同,然后检查是否这个误差是否低到可以忍受的级别。

也有一些情况是病理的,这些是由于JIT优化导致。查看下面的代码:

using System;
class Test
{
    static float f;
    static void Main(string[] args)
    {
        f = Sum (0.1f, 0.2f);
        float g = Sum (0.1f, 0.2f);
        Console.WriteLine (f==g);
       //g = g + 1;
    }
    static float Sum (float f1, float f2)
    {
        return f1+f2;
    }
}

  它应该总是打印True, 对不?错,很不幸。当在debug模式下运行时,JIT不能像正常那样做一些优化处理,它将打印True.当正常运行时JIT可以将sum 的结果存储的比一个float可以实际表示的数更加精确 。

   它可以使用默认x86 80位表示,例如,对sum 本身,返回值和本地变量。查看ECMA CLI 规范,第一部分, 12.1.3 章节来获得更多细节。取消上面的注释,让JIT的行为稍微谨慎一些 - 结果将会是True - 尽管在当前的实现可以让结果是True,但是不应该被信赖.(在上面语句中将g强制转换成float也可以有同样的效果,尽管它看起来像一个空操作(no-op).)

  这是另外的避免对浮点数做相等比较的原因,尽管你非常确定结果应该是一样的。

  (译者注: .NET 平台的运行结果总是True. Java 平台没有自己做过测试,别人的测试也是True)

  .NET 是如何格式化浮点数的?

  在.NET中没有查看一个浮点数的精确十进制值的内建方式,尽管你可以通过一些工作来完成。(查看这篇文章的末尾的一些可以实现这个功能的代码。)默认情况下,.NET将一个double类型数格式化成15个十进制位置,将一个float类型数格式化成7个十进制位置。(在一些情况将使用科学计数法;查看MSDN标准数字格式字符串页来获得更多内容。)如果你使用往返模式规范(“r”),它会将数字格式化成最短格式,当截取(成同样类型)时,将会变成初始数字。如果你以字符串存储浮点数字而且精确的值对你来说很重要,你应该定义使用往返模式规范,否则你非常可能丢失数据。

  一个浮点数在内存中看起来究竟是什么样子的?

  正如上面所说的,一个浮点数基本有一个符号位,一个指数和一个尾数。所有这些都是整数,它们三个的联合精确的确定数字的表示形式。有很多浮点数类别: 规范数,低于正常数,无穷数和非数字(NaN, not a number).大多数数字是规范化的,意味着二进制尾数位的第一位是1,也意味着你实际上不需要存储它。例如,二进制数1.01101可以仅用.01101表示 - 开始的1是假设的,如果是0将会使用一个不同的指数。那个技术只有当数字在可以选择适合的指数范围时才可以工作。不在那个范围中的数字(非常,非常小的数字)被称为非正常数字,并假设没有开始位。”不是一个数字”(NaN, not a number)是像指0/0的结果之类的,等等。NaN有很多不同的类别,也有一些老的行为。非正常数字有时候也称作非规范数。

  符号位,指数和尾数在比特级别的表示方法都是一个无符号整数,存储的值按顺序先是符号位,然后是指数位,最后是尾数。”真实的”指数是有偏移值的 - 例如,一个double型数,指数是1023偏移,所以当你回来计算出实际值时,一个存储指数值为1026的值就变成3。下面的表显示了符号位,指数和尾数的每种组合的意思,使用double作为一个例子。相同的原则也适用于float,仅有一些不同值(比如偏移值不同)。注意这里给出的指数值是指存储的指数,在偏移值应用之前。(那就是为什么偏移值显示在”值”列。)


符号位(s, 1位)


存储的指数(e, 11位)


尾数(m, 52位)


数字类型


任意 非零 任意 正常 (-1)s x 1.m (二进制) x 2e-1023
0 0 0 0 +0
1 0 0 0 +0
0 2047 0 无穷大 正无穷大
1 2047 0 无穷大 负无穷大
0 2047 非零 非数字 n/a

  可以工作的例子

  考虑下面的64位二进制数:

0100000001000111001101101101001001001000010101110011000100100011

  作为一个double型数,可以被拆分成:

  符号位: 0

  指数位: 10000000100 二进制=1028 十进制

  尾数位: 0111001101101101001001001000010101110011000100100011

这是因此一个正常数的值

(-1)0 x 10111001101101101001001001000010101110011000100100011 (binary) x 21028-1023

也可以更简单的表示为

1.0111001101101101001001001000010101110011000100100011 (binary) x 25

或者

101110.01101101101001001001000010101110011000100100011

在十进制,这是46.42829231507700882275457843206822872161865234375,但是.NET 将会默认显示46.428292315077 或者使用”往返”格式规范表示为46.428292315077009.

  NaNs

  NaNs 是奇兽。有两种类型的NaNs - 信号和安静(signalling and quiet, 译意可能不准确)或者简短表示为SNan和QNaN。在位模式概念中,一个安静的NaN有高位尾数, 而一个信号NaN将它清除了。安静NaNs用来标记精确操作是未定义的,而信号NaNs用来定义其他的(操作是非法的,而不是仅有一个不确定输出)。

大多数人想知道的最奇怪的事情时NaNs不等于它们自己。例如,Double.NaN==Double.NaN 结果是false.相反,你需要使用Double.NaNs来检查是否一个值不是一个数字。幸运的是,大多数人不可能遇到NaNs除了在这篇文章里。

  结论

  只要你知道发生了什么并且不期望你在你的程序中输入的十进制数就是十进制数值,并且不期望设计二进制浮点数的计算必须生成精确结果,那么二进制浮点算术是很好的。尽管两个数字都被你正在使用的类型精确表示,涉及这两个数的操作结果将不会必须精确表示。这个可以很简单的通过除法操作(例如1/10 不是精确表示的,但1 和10都是精确表示的)看出来但是它可以在任何操作中发生 - 尽管看起来不可能发生的如加法和减法操作。

  如果你特别需要精确十进制数字,考虑使用decimal类型来代替 - 但是这样做要考虑到付出性能的代价。(一个非常快设计的测试显示doubles类型数的乘法比decimals类型的乘法快40倍;不要为这个情况花费额外的注意,但是要将在当前硬件环境里二进制浮点运算比十进制浮点运算快很多作为一个提示看待。)

  以我的经验来看,大多数商业应用可能有很多种类的用十进制浮点数比二进制浮点更好的值。特别的,几乎任何要与钱相关的数字都更适合使用decimal表示。

时间: 2024-07-31 16:28:39

一起谈.NET技术,.NET 中的二进制浮点类型的相关文章

.NET 中的二进制浮点类型

大多数人会对他们在.NET中的算术的"出错"首先感到惊讶.使用一些称为"浮点"算术来表示非整型数字不是.NET 相比其他大多数语言/平台特殊的地方.在.NET 内部是没问题的,但是你需要知道一些底层正在发生什么,否则你将会对一些结果感到惊讶. 我在这个事情上不是一个专家这不重要.虽然写了这篇文章,我也发现了另外一篇 - 这次是一个真正的专家写的,杰弗里 萨克斯(Jeffrey Sax).我强烈建议你也同时读他的浮点文章. 什么是浮点数? 计算机总是需要一些表示数据的

一起谈.NET技术,中软面试题-最新

      中软的面试比较经典,也比较严格,一般有四轮,类似于微软的面试.中软面过以后,根据项目组,会推到美国微软那边运用live meeting & con-call 再面一次.以下是我的面试题及个人的小分析,拿出来和大家share一下.希望更多的人能过这个坎.如有什么问题,可以一起交流.直接进入主题:  1. English communication. (sale yourself, project information, your interesting,and how to deal

一起谈.NET技术,从.NET中委托写法的演变谈开去(上):委托与匿名方法

在<关于最近面试的一点感想>一文中,Michael同学谈到他在面试时询问对方"delegate在.net framework1.1,2.0,3.5各可以怎么写"这个问题.于是乎,有朋友回复道"请问楼主,茴香豆的茴有几种写法","当代孔乙己",独乐,众乐.看了所有的评论,除了某些朋友认为"的确不该不知道这个问题"之外,似乎没有什么人在明确支持楼主. 不过我支持,为什么?因为我也提过出这样的问题. 这样,我们暂且不提应

一起谈.NET技术,从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势

在上一篇文章中我们简单探讨了.NET 1.x和.NET 2.0中委托表现形式的变化,以及.NET 2.0中匿名方法的优势.目的及注意事项.那么现在我们来谈一下.NET 3.5(C# 3.0)中,委托的表现形式又演变成了什么样子,还有什么特点和作用. .NET 3.5中委托的写法(Lambda表达式) Lambda表达式在C#中的写法是"arg-list => expr-body","=>"符号左边为表达式的参数列表,右边则是表达式体(body).参数列表

一起谈.NET技术,.Net Discovery系列之-深入理解平台机制与性能影响 (中)

上一篇文章中Aicken为大家介绍了.Net平台的垃圾回收机制与其对性能的影响,这一篇中将继续为大家介绍.Net平台的另一批黑马-JIT.有关JIT的机制分析 ● 机制分析以C#为例,在C#代码运行前,一般会经过两次编译,第一阶段是C#代码向MSIL的编译,第二阶段是IL向本地代码的编译.第一阶段的编译成果是生成托管模块,第二阶段的编译成果是生成本地代码以供运行,从这里各位同学可以看出,第一阶段生成的MSIL是不能直接运行的.必须指出的是JIT在第一次编译IL后,会修改对应方法相应的内存地址入口

一起谈.NET技术,详解ASP.NET MVC 2中的新ADO.NET实体框架

.NET框架4.0的发行推出了许多优秀的增强功能,其中当首推ADO.NET实体框架.该框架已经克服了以前的许多错误,并提供了一组增强的API,其中包括许多新的LINQ to SQL框架方面的改善.在本文中,我们将使用这些API的功能来创建一个通用版本的数据仓库. 一.实体框架概述 实体框架针对数据模型提供了一些更方便的操作方法.默认情况下,设计器可以生成一个描述数据库的模型. 尽管表格间的映射未必都是1:1的映射.每个表格使用一个ObjectSet加以描述,进而ObjectSet对象又提供了相应

浅谈C语言编程中程序的一些基本的编写优化技巧_C 语言

大概所有学习C语言的初学者,都被前辈说过,C语言是世界上接近最速的编程语言,当然这并不是吹牛,也并不是贬低其他语言,诚然非C语言能写出高速度的代码,但是C语言更容易写出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡没落. 对于现代编译器,现代CPU而言,我们要尽量迎合CPU的设计(比如架构和处理指令的方式等),虽然编译器是为程序员服务,并且在尽它最大的能力来优化程序员写出的代码,但是毕竟它还没有脱离电子的范畴,如果我们的代码不能让编译器理解,编译器无法帮我们优化代码,那么

《创业家》牛文文:少谈点模式多谈点技术

"模式"如同当年的"主义",流行于各种创业大赛.创业励志节目.论坛的"街头"式秀场 文/创业家 牛文文 "美国某某公司你知道吧?就是刚被戴尔.惠普.思科十几亿美元抢购的那家.我们的模式和它的一样,现在还没赢利,可将来起码有十几亿人民币的市值." "我开了小煤矿,但煤运不出去,上商学院之后受到启发,想搞模式创新,具体讲就是想在铁路边上搞个煤炭物流开发区,建一个大的物流和信息流平台,把分散的煤炭集中在我这个园区,这样和铁

MSDN 杂志:UI 前沿技术 - WPF 中的多点触控操作事件

原文  MSDN 杂志:UI 前沿技术 - WPF 中的多点触控操作事件 UI 前沿技术 WPF 中的多点触控操作事件 Charles Petzold 下载代码示例 就在过去几年,多点触控还只是科幻电影中表现未来主义的一种重要手法,现在俨然已经成为主流的用户界面技术. 多点触控显示屏现在成了新型智能手机和 Tablet 计算机的标准显示屏. 此外,它还可能在公共场所的计算机上普及,例如 Microsoft Surface 率先开发的网亭或桌面计算机. 实际存在的唯一不确定因素是多点触控在常规台式