博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
浅谈 System.Decimal 结构
阅读量:6905 次
发布时间:2019-06-27

本文共 6064 字,大约阅读时间需要 20 分钟。

引言

我们知道,Microsoft .NET Framework 中的 System.Decimal 结构(在 C# 语言中等价于 decimal 关键字)用来表示十进制数,范围从 -(296 - 1) 到 296 - 1,并且可以有 28 位小数。这就是说:

  • decimal.MinValue = -79,228,162,514,264,337,593,543,950,335 = -(296 - 1)
  • decimal.MaxValue = 79,228,162,514,264,337,593,543,950,335 = 296 - 1
  • decimal.Epsilon = 0.0000000000000000000000000001 = 10-28

上面前两个是 decimal 的静态只读字段。遗憾的是,第三个不属于 decimal 结构。

decimal 内部使用 4 个 32-bit 的 System.Int32 来存储,占用 128 bits = 16 bytes。这 128 bits 分配如下:

  • 96 bits 表示从 0 至 296 - 1 的整数,分布在 3 个 32-bit 的 System.Int32 中。
  • 剩下的 1 个 32-bit 的 System.Int32 包括符号位和比例因子。
  • 第 31 bit 是符号位,0 表示正数,1 表示负数。
  • 第 16 至 23 bit 表示比例因子,必须包含一个 0 至 28 之间的指数,指示 10 的幂,即小数点的位置,也就是小数点右边有几位数字。
  • 其实表示 0 至 28 之间的指数只需 5 bits 就够了,而上面的第 16 至 23 bit 共 8 bits = 1 byte。也就是说剩下的 3 bits (第 21 至 23 bit) 一定是零。
  • 其余 bits (0 - 15 bit 和 24 - 30 bit)不被使用,必须为零。

decimal.GetBits 方法就返回上述 decimal 的内部表示。而 decimal (int[] bits) 构造函数就使用这个内部表示构造来构造 decimal 实例。一个 decimal 可能会有几种不同的内部表示,所有这些内部表示均同样有效,并且在数值上相等。

TinyDecimal 数据类型

为了更好地理解 decimal 结构,我们来构造一个只有 8 bits = 1 byte 的 TinyDecimal 结构:

  • number: 第 0 至 5 bit (共 6 bits)表示从 0 至 26 - 1 的整数,共有 64 个。
  • exp: 第 6 bit 表示比例因子,包含一个 0 至 1 之间的指数,指示 10 的幂,即小数点的位置。0 表示小数点在最右边。
  • sign: 第 7 bit 是符号位,0 表示正数,1 表示负数。

因此:

  • TinyDecimal.MinValue = -63 = -(26 - 1)
  • TinyDecimal.MaxValue = 63 = 26 - 1
  • TinyDecimal.Epsilon = 0.1 = 10-1

也就是说,TinyDecimal 的表示范围从 -63 至 63,并且可以有 1 位小数。

TinyDecimal 的正数有以下两种情形:

  • 当 exp = 1 时: 0.1, 0.2, ... , 0.9, 1.0, 1.1, ... , 6.2, 6.3 。共 63 个。
  • 当 exp = 0 时:1, 2, ... , 63 。共 63 个,但前 6 个(1 = 1.0, 2 = 2.0, ... , 6 = 6.0)和上面的重复了。

所以 TinyDecimal 的正数共有 63 + (63 - 6) = 120 个。负数的情况是一样的,也有 120 个。所以 TinyDecimal 有 241 个不同的值,即正数和负数各 120 个,加上一个零。注意,零有四种不同的表示:+0, -0, +0.0, -0.0。TinyDecimal 的正数顺序排列如下:

  • 0.1, 0.2, ... , 6.2, 6.3, 7, 8, 9, 10, 11, ... , 62, 63

注意,在 TinyDecimal 中,6.3 的下一个数就是 7,7 的下一个数就是 8,根据就不存在 6.4 和 7.1 之类的数。并且有以下运算例子:

  • 6.3 + 0.1 = 6.3
  • 6.3 + 0.3 = 7
  • 7 + 0.4 = 7
  • 7 + 0.6 = 8
  • 63 + 1 = overflow

我们知道,1 byte 可以表示 28 = 256 个不同的值。而 TinyDecimal 有 241 个不同的值,计算如下:241 = 256 - 6 * 2 - 3 ,即需要扣除 6 * 2 个重复的正负数和 3 个重复的零。

测试程序

System.Decimal 结构就是以上 TinyDecimal 结构的放大版本。为了更好地理解以上内容,我写了一个如下所测试程序:

1 using System; 2   3 static class DecimalTester 4 { 5   static void Main() 6   { 7     var epsilon = 0.0000000000000000000000000001m; 8     var a = decimal.MaxValue / 100; 9     var b = 7.1234567890123456789012345685m;10     Console.WriteLine("{0}: 1e-28", epsilon);11     Console.WriteLine("{0}: 1e-28 + 0.1", 0.1m + epsilon);12     Console.WriteLine("{0}: a", a);13     Console.WriteLine("{0,-30}: a + 0.004", a + 0.004m);14     Console.WriteLine("{0,-30}: a + 0.005", a + 0.005m);15     Console.WriteLine("{0,-30}: a + 0.01", a + 0.01m);16     Console.WriteLine("{0,-30}: a + 0.099", a + 0.099m);17     Console.WriteLine("{0,-30}: a + 0.1", a + 0.1m);18     Console.WriteLine("{0,-30}: (a + 0.1) + 1e-28", a + 0.1m + epsilon);19     Console.WriteLine("{0,-30}: a + (0.1 + 1e-28)", a + (0.1m + epsilon));20     Console.WriteLine("{0}: b", b);21     Console.WriteLine("{0,-30}: b + 1", b + 1);22   }23 }

这个程序第 7 行的 epsilon 就是引言中提到的 decimal.Epsilon,其值为 10-28,等于 decimal 能够表示最小正数。

在 Linux 中编译和运行

在 Arch Linux 64-bit 操作系统的 Mono 3.0.4 环境下编译和运行:

work$ dmcs --versionMono C# compiler version 3.0.4.0work$ dmcs DecimalTester.cswork$ mono DecimalTester.exe0.0000000000000000000000000001: 1e-280.1000000000000000000000000001: 1e-28 + 0.1792281625142643375935439503.35: a792281625142643375935439503.35: a + 0.004792281625142643375935439503.4 : a + 0.005792281625142643375935439503.4 : a + 0.01792281625142643375935439503.4 : a + 0.099792281625142643375935439503.5 : a + 0.1792281625142643375935439503.5 : (a + 0.1) + 1e-28792281625142643375935439503.5 : a + (0.1 + 1e-28)7.1234567890123456789012345685: b8.123456789012345678901234569 : b + 1

上述运行结果各行对应如下:

  1. epsilon = 10-28 = 0.0000000000000000000000000001,这是 decimal 能够表示的最小正数。
  2. epsilon + 0.1 = 10-28 + 0.1 = 0.1000000000000000000000000001 。
  3. a = decimal.Value / 100 = 79...3.35 。这个数有 29 位有效数字。
  4. a + 0.004 = 79...3.354,舍入至 79...3.350,就等于 a 。
  5. a + 0.005 = 79...3.355,舍入至 79...3.360,但这个数无法在 decimal 中表示,只好舍入至 79...3.400,这个数只有 28 位有效数字。
  6. a + 0.01 = 79...3.36,如上所述,这个数无法在 decimal 中表示,只好舍入至 79...3.40
  7. a + 0.099 = 79...3.449,舍入至 79...3.400
  8. a + 0.1 = 79...3.45,舍入至 79...3.50
  9. (a + 0.1) + 10-28,结果和上一行相同。
  10. a + (0.1 + 10-28),结果和上一行相同。
  11. b = 7.1234567890123456789012345685 。这个数有 29 位有效数字。
  12. b + 1 = 8.1...85 。但这个数无法在 decimal 中表示,只好舍入至 8.1...90

从上面的分析可以看出,在 Linux 的 Mono 环境中 decimal 的算术运算的舍入规则是四舍五入。

在 Windows 中编译和运行

在 Windows 7 SP1 32-bit 操作系统的 Microsoft .NET Framework 4.5 环境下编译和运行:

D:\work> csc DecimalSumTester.csMicrosoft(R) Visual C# 编译器版本 4.0.30319.17929用于 Microsoft(R) .NET Framework 4.5版权所有 (C) Microsoft Corporation。保留所有权利。D:\work> DecimalTester0.0000000000000000000000000001: 1e-280.1000000000000000000000000001: 1e-28 + 0.1792281625142643375935439503.35: a792281625142643375935439503.35: a + 0.004792281625142643375935439503.4 : a + 0.005792281625142643375935439503.4 : a + 0.01792281625142643375935439503.4 : a + 0.099792281625142643375935439503.4 : a + 0.1792281625142643375935439503.4 : (a + 0.1) + 1e-28792281625142643375935439503.5 : a + (0.1 + 1e-28)7.1234567890123456789012345685: b8.123456789012345678901234568 : b + 1

上述运行结果各行对应如下:

  1. epsilon = 10-28 = 0.0000000000000000000000000001,这是 decimal 能够表示的最小正数。
  2. epsilon + 0.1 = 10-28 + 0.1 = 0.1000000000000000000000000001 。
  3. a = decimal.Value / 100 = 79...3.35 。这个数有 29 位有效数字。
  4. a + 0.004 = 79...3.354,舍入至 79...3.350,就等于 a 。
  5. a + 0.005 = 79...3.355,舍入至 79...3.360,但这个数无法在 decimal 中表示,只好舍入至 79...3.400,这个数只有 28 位有效数字。
  6. a + 0.01 = 79...3.36,如上所述,这个数无法在 decimal 中表示,只好舍入至 79...3.40 。
  7. a + 0.099 = 79...3.449,舍入至 79...3.400 。
  8. a + 0.1 = 79...3.45,舍入至 79...3.40 。
  9. (a + 0.1) + 10-28,结果和上一行相同,等于 79...3.40 。因为 10-28 太小了,加上去也改变不了什么。
  10. a + (0.1 + 10-28) = 79...3.4500000000000000000000000001,舍入至 79...3.50...0 。和上一行对比,发现加法不满足结合律。
  11. b = 7.1234567890123456789012345685 。这个数有 29 位有效数字。
  12. b + 1 = 8.1...85 。但这个数无法在 decimal 中表示,只好舍入至 8.1...80 。

从上面的分析可以看出,在 Windows 的 .NET Framework 环境中 decimal 的算术运算的舍入规则是四舍六入五取偶。所以造成第 8 、9 和 12 行和 Linux 中的输出不同。

由于 decimal 的精度是有限的,只能表示有限个分散的值,在进行一些特殊的算术运算步骤时,会产生非常出乎意料的结果。且听下回分解。

参考资料

转载地址:http://mmrdl.baihongyu.com/

你可能感兴趣的文章
CentOS Rsync服务端与Windows cwRsync客户端实现数据同步
查看>>
TAR包CLONE方式 安装11GR2 RAC数据库 (gril软件root.sh)
查看>>
热备份路由协议(HSRP)大型配置之详解
查看>>
gb2312编码文件转成utf8
查看>>
一次WinRoute后门攻防实况
查看>>
数据驱动业务决策的5个步骤
查看>>
上网个人信息如何不“裸奔” 10条信息安全建议
查看>>
JIRA的常用选项
查看>>
专访Facebook HipHop作者、阿里研究员赵海平:生物与计算机交织的独特人生
查看>>
监控视频须严加规范
查看>>
实例化需求的优点
查看>>
Linux管理常见错误的解决方法
查看>>
MySQL架构优化实战系列3:定时计划任务与表分区
查看>>
kafka - advertised.listeners and listeners
查看>>
Hadoop YARN学习监控JVM和实时监控Ganglia、Ambari(5)
查看>>
ECharts:免费,开源,超炫的可视化作品
查看>>
跨界 +赋能——互联网的下一个关键词
查看>>
argz_create函数
查看>>
vmware HA与vmware FT功能对比
查看>>
分区表添加分区的问题
查看>>