.“.NET研究”NET 中的二进制浮点类型

简介:   大多数人会对他们在.NET中的算术的"出错"首先感到惊讶。使用一些称为”浮点”算术来表示非整型数字不是.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)

  .NE上海企业网站设计与制作T 中的浮点类型是什么样子的?

  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 2 e-1023 0 0 0 0 +0 1 0 0 0 +0 0 2047 0 无穷大 正无穷大 1 2047 0 上海徐汇企业网站制作://www.93tj.com'>上海企业网站制作align="top">无穷大 负无穷大 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.428292315077上海闵行企业网站设计与制作009.

  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表示。

目录
相关文章
|
4月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
142 7
|
4月前
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
|
5月前
|
开发框架 .NET API
.NET Core 和 .NET 标准类库项目类型有什么区别?
在 Visual Studio 中,可创建三种类库:.NET Framework、.NET Standard 和 .NET Core。.NET Standard 是规范,确保跨.NET实现的API一致性,适用于代码共享。.NET Framework 用于特定技术,如旧版支持。.NET Core 库允许访问更多API但限制兼容性。选择取决于兼容性和所需API:需要广泛兼容性时用.NET Standard,需要更多API时用.NET Core。.NET Standard 替代了 PCL,促进多平台共享代码。
|
6月前
|
机器学习/深度学习 JSON 测试技术
CNN依旧能战:nnU-Net团队新研究揭示医学图像分割的验证误区,设定先进的验证标准与基线模型
在3D医学图像分割领域,尽管出现了多种新架构和方法,但大多未能超越2018年nnU-Net基准。研究发现,许多新方法的优越性未经严格验证,揭示了验证方法的不严谨性。作者通过系统基准测试评估了CNN、Transformer和Mamba等方法,强调了配置和硬件资源的重要性,并更新了nnU-Net基线以适应不同条件。论文呼吁加强科学验证,以确保真实性能提升。通过nnU-Net的变体和新方法的比较,显示经典CNN方法在某些情况下仍优于理论上的先进方法。研究提供了新的标准化基线模型,以促进更严谨的性能评估。
177 0
|
7月前
|
安全 API C#
C#.Net筑基-类型系统②常见类型--枚举Enum
枚举(enum)是C#中的一种值类型,用于创建一组命名的整数常量。它们基于整数类型(如int、byte等),默认为int。枚举成员可指定值,未指定则从0开始自动递增。默认值为0。枚举可以与整数类型互相转换,并可通过`[Flags]`特性表示位域,支持位操作,用于多选场景。`System.Enum`类提供了如`HasFlag`、`GetName`等方法进行枚举操作。
|
7月前
|
编译器 C#
C#.Net筑基-类型系统②常见类型 --record是什么类型?
`record`在C#中是一种创建简单、只读数据结构的方式,常用于轻量级数据传输。它本质上是类(默认)或结构体的快捷形式,包含自动生成的属性、`Equals`、`ToString`、解构赋值等方法。记录类型可以继承其他record或接口,但不继承普通类。支持使用`with`语句创建副本。例如,`public record User(string Name, int Age)`会被编译为包含属性、相等比较和`ToString()`等方法的类。记录类型提供了解构赋值和自定义实现,如密封的`sealed`记录,防止子类重写。
|
7月前
|
存储 C#
C#.Net筑基-类型系统②常见类型--结构体类型Struct
本文介绍了C#中的结构体(struct)是一种用户自定义的值类型,适用于定义简单数据结构。结构体可以有构造函数,能定义字段、属性和方法,但不能有终结器或继承其他类。它们在栈上分配,参数传递为值传递,但在类成员或包含引用类型字段时例外。文章还提到了`readonly struct`和`ref struct`,前者要求所有字段为只读,后者强制结构体存储在栈上,适用于高性能场景,如Span和ReadOnlySpan。
|
7月前
|
存储 安全 Unix
C#.Net筑基-类型系统②常见类型--日期和时间的故事
在System命名空间中,有几种表示日期时间的不可变结构体(Struct):DateTime、DateTimeOffset、TimeSpan、DateOnly和TimeOnly。DateTime包含当前本地或UTC时间,以及最小和最大值;DateTimeOffset增加了时区偏移信息,适合跨时区操作。UTC是世界标准时间,而格林尼治标准时间(GMT)不稳定,已被更精确的UTC取代。DateTimeOffset和DateTime提供了转换为UTC和本地时间的方法,以及各种解析和格式化函数。
|
6月前
|
存储 编译器
【.NET Core】可为null类型详解
【.NET Core】可为null类型详解
229 0
|
7月前
|
机器学习/深度学习 算法 数据可视化
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究