引言

我们知道,Microsoft .NET Framework 中的 System.Decimal 结构(在 C# 语言中等价于 decimal keyword)用来表示十进制数,范围从 -(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. 1 using System;
  2. 2
  3. 3 static class DecimalTester
  4. 4 {
  5. 5 static void Main()
  6. 6 {
  7. 7 var epsilon = 0.0000000000000000000000000001m;
  8. 8 var a = decimal.MaxValue / 100;
  9. 9 var b = 7.1234567890123456789012345685m;
  10. 10 Console.WriteLine("{0}: 1e-28", epsilon);
  11. 11 Console.WriteLine("{0}: 1e-28 + 0.1", 0.1m + epsilon);
  12. 12 Console.WriteLine("{0}: a", a);
  13. 13 Console.WriteLine("{0,-30}: a + 0.004", a + 0.004m);
  14. 14 Console.WriteLine("{0,-30}: a + 0.005", a + 0.005m);
  15. 15 Console.WriteLine("{0,-30}: a + 0.01", a + 0.01m);
  16. 16 Console.WriteLine("{0,-30}: a + 0.099", a + 0.099m);
  17. 17 Console.WriteLine("{0,-30}: a + 0.1", a + 0.1m);
  18. 18 Console.WriteLine("{0,-30}: (a + 0.1) + 1e-28", a + 0.1m + epsilon);
  19. 19 Console.WriteLine("{0,-30}: a + (0.1 + 1e-28)", a + (0.1m + epsilon));
  20. 20 Console.WriteLine("{0}: b", b);
  21. 21 Console.WriteLine("{0,-30}: b + 1", b + 1);
  22. 22 }
  23. 23 }

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

在 Linux 中编译和执行

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

  1. work$ dmcs --version
  2. Mono C# compiler version 3.0.4.0
  3. work$ dmcs DecimalTester.cs
  4. work$ mono DecimalTester.exe
  5. 0.0000000000000000000000000001: 1e-28
  6. 0.1000000000000000000000000001: 1e-28 + 0.1
  7. 792281625142643375935439503.35: a
  8. 792281625142643375935439503.35: a + 0.004
  9. 792281625142643375935439503.4 : a + 0.005
  10. 792281625142643375935439503.4 : a + 0.01
  11. 792281625142643375935439503.4 : a + 0.099
  12. 792281625142643375935439503.5 : a + 0.1
  13. 792281625142643375935439503.5 : (a + 0.1) + 1e-28
  14. 792281625142643375935439503.5 : a + (0.1 + 1e-28)
  15. 7.1234567890123456789012345685: b
  16. 8.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 环境下编译和执行:

  1. D:\work> csc DecimalSumTester.cs
  2. Microsoft(R) Visual C# 编译器版本号 4.0.30319.17929
  3. 用于 Microsoft(R) .NET Framework 4.5
  4. 版权全部 (C) Microsoft Corporation
  5.  
  6. 保留全部权利。
  7. D:\work> DecimalTester
  8. 0.0000000000000000000000000001: 1e-28
  9. 0.1000000000000000000000000001: 1e-28 + 0.1
  10. 792281625142643375935439503.35: a
  11. 792281625142643375935439503.35: a + 0.004
  12. 792281625142643375935439503.4 : a + 0.005
  13. 792281625142643375935439503.4 : a + 0.01
  14. 792281625142643375935439503.4 : a + 0.099
  15. 792281625142643375935439503.4 : a + 0.1
  16. 792281625142643375935439503.4 : (a + 0.1) + 1e-28
  17. 792281625142643375935439503.5 : a + (0.1 + 1e-28)
  18. 7.1234567890123456789012345685: b
  19. 8.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 的精度是有限的,仅仅能表示有限个分散的值。在进行一些特殊的算术运算步骤时,会产生很出乎意料的结果。

且听下回分解。

浅谈 System.Decimal 结构的更多相关文章

  1. 浅谈c语言结构体

    对于很多非计算机专业来说,c语言课程基本上指针都不怎么讲,更别说后面的结构体了.这造成很多学生对结构体的不熟悉.这里我就浅谈一下我对结构体的认识. 结构体,就是我们自己定义出一种新的类型,定义好之后, ...

  2. 浅谈C/C++结构体内存分配问题

    .wiz-todo, .wiz-todo-img {width: 16px; height: 16px; cursor: default; padding: 0 10px 0 2px; vertica ...

  3. 浅谈System.gc()

      今天巩固给大家讲讲System.gc().Java的内存管理着实给各位编程者带来很大的方便,使我们不再需要为内存分配烦太多神.那么讲到垃圾回收机制,就不得不讲讲System.gc().   先简单 ...

  4. 浅谈JVM - 内存结构(二)- 虚拟机栈|凡酷

    2.1 定义 Java Virtual Machine Stacks(Java虚拟机栈) Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧,是线程私有的,生命周期随着线程启动而产 ...

  5. 浅谈linux虚拟内存结构

    一个虚拟存储器系统要求硬件和软件之间紧密写作(mmu(内存管理单元,虚拟地址到物理地址的翻译),TLB块表(虚拟地址到物理地址index,虚拟寻址),l1,l2,l3高速缓存(物理单元数据)物理寻址) ...

  6. 【.Net基础二】浅谈引用类型、值类型和装箱、拆箱

    目前在看CLR via C#,把总结的记下来,索性就把他写成一个系列吧. 1.[.Net基础一] 类型.对象.线程栈.托管堆运行时的相互关系 2.[.Net基础二]浅谈引用类型.值类型和装箱.拆箱 引 ...

  7. 浅谈PHP代码设计结构

    浅谈PHP代码设计结构 您的评价:       还行  收藏该经验       coding多年,各种代码日夜相伴,如何跟代码友好的相处,不光成为职业生涯的一种回应,也是编写者功力的直接显露. 如何看 ...

  8. 浅谈oracle树状结构层级查询之start with ....connect by prior、level及order by

    浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...

  9. 浅谈oracle树状结构层级查询测试数据

    浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...

随机推荐

  1. mysql在linux下的安装(5.7版本以后)

    1.添加mysql组和mysql用户,用于设置mysql安装目录文件所有者和所属组. ①groupadd mysql ②useradd -r -g mysql mysql 2.将二进制文件解压到指定的 ...

  2. azkaban-executor启动时出现conf/global.properties (No such file or directory)的问题解决(图文详解)

     问题详情 // :: INFO [FlowRunnerManager] [Azkaban] Cleaning recently finished // :: INFO [FlowRunnerMana ...

  3. ios cocos2d-x 多点触摸

    需要在 AppController.mm文件中添加一句多点触摸支持: [_view setMultipleTouchEnabled:true];

  4. Ambari?自动部署Hadoop集群

    自动部署?Ambari Ambari 跟 Hadoop 等开源软件一样,也是 Apache Software Foundation 中的一个项目,并且是顶级项目.就 Ambari 的作用来说,就是创建 ...

  5. JVM 优化之逃逸分析

    整理自 周志明<深入JVM> 1, 是JVM优化技术,它不是直接优化手段,而是为其它优化手段提供依据. 2,逃逸分析主要就是分析对象的动态作用域. 3,逃逸有两种:方法逃逸和线程逃逸.   ...

  6. SAS学习笔记之《SAS编程与数据挖掘商业案例》(2)数据获取与数据集操作

    SAS学习笔记之<SAS编程与数据挖掘商业案例>(2)数据获取与数据集操作 1. SET/SET效率高,建立的主表和建表索引的查询表一般不排序, 2. BY语句,DATA步中,BY语句规定 ...

  7. (转)Hibernate快速入门

    http://blog.csdn.net/yerenyuan_pku/article/details/64209343 Hibernate框架介绍 什么是Hibernate 我们可以从度娘上摘抄这样有 ...

  8. centos7安装个人网盘owncloud

    现在个人资料越来越重要,网络速度也已经满足日常需要,网盘已经是生活着存取个人数据不可缺少的工具. 下面在linxu centos7下面安装owncloud搭建自己私人网盘: 1.新建一个账号用来安装个 ...

  9. 【Redis】一、Redis简介及五种数据类型

    (一)Redis简介   Redis(Remote Dictionary Server)是一个使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化的日志型.Key-Value的开源 ...

  10. hdu 2084 数塔(简单dp)

    题目 简单dp //简单的dp #include<stdio.h> #include<string.h> #include<algorithm> using nam ...