作者:沈寅
原文链接:https://www.jianshu.com/p/ac337d1869ec
综上所述,NEO系统中数值的范围是
NEO:0~100000000,精确度为1
GAS: 0.00000001~100000000.00000000
计算机系统一般来说是对现实世界的模拟,用来运算一个现实中遇到的问题,那么现实世界和计算机系统一般就有一个映射关系,我们现实世界中的各种数学概念中的数,在计算机中都要有一种表达方式。由于我的理论知识并不好,只能简单的描述一下我所认识到的计算机世界:
整数在C#中是int,long,ulong等类型,我们看一下他的表示范围。
64位机器上C#类型 | 范围 |
---|---|
int | Signed: From −2,147,483,648 to 2,147,483,647, from −(231) to 231 − 1 |
long | Signed: From −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807, from −(263) to 263 − 1 |
在C#中,int的范围是正负20亿左右(十进制),long的范围是
2的63次方=9223372036854775808,如果以米作单位,大约为光走一千年的距离;如果以秒作单位,大约为3000亿年,十进制是19位的一个数字。
由于Neo最大量是1亿,最小单位为1,在64位机器上做运算基本不会溢出,所以直接用long我觉得也是可以的。
浮点(英语:floating point,缩写为FP)是一种对于实数的近似值数值表现法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数(floating-point number)。利用浮点进行运算,称为浮点计算,这种运算通常伴随着因为无法精确表示而进行的近似或舍入。
在电脑使用的浮点数被电气电子工程师协会(IEEE)规范化为IEEE 754。
并不是说小数叫做浮点数。准确的来说:“浮点数”是一种表示数字的标准,整数也可以用浮点数的格式来存储。
在C#的程序语言中,浮点数用float和double来表示。
EEE 754 规定,浮点数的表示方法为:
整型和浮点型都是64位的情况下,浮点型表示的范围更大,但是虽然范围大,可以表示的数字的数量,却是根据表示数的二进制位确定的。
从上表可以看出,decimal的有效位数很大,达到了28位,但是表示的数据范围却比float和double类型小。decimal类型并不是C#中的基础类型,所以使用的时候会对计算时的性能有影响。由于精度高,更适合用来处理金融系统的计算问题。但是其本身计算还是有误差的,精度再高都是实数轴上的离散点。
Type | Approximate range | Precision | .NET Framework type |
---|---|---|---|
double |
±5.0 × 10−324 to ±1.7 × 10308 | 15-16 digits | System.Double |
float |
-3.4 × 1038to +3.4 × 1038 | 7 digits | System.Single |
decimal |
(-7.9 x 1028 to 7.9 x 1028) / (100 to 1028) | 28-29 significant digits | System.Decimal |
Type | Range | Size | .NET Framework type |
---|---|---|---|
int |
-2,147,483,648 to 2,147,483,647 | Signed 32-bit integer | System.Int32 |
uint |
0 to 4,294,967,295 | Unsigned 32-bit integer | System.UInt32 |
long |
-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Signed 64-bit integer | System.Int64 |
ulong |
0 to 18,446,744,073,709,551,615 | Unsigned 64-bit integer | System.UInt64 |
sbyte |
-128 to 127 | Signed 8-bit integer | System.SByte |
short |
-32,768 to 32,767 | Signed 16-bit integer | System.Int16 |
为了更进一步的讨论计算机中的数字,我们需要搞清楚范围,精度,准确性这三个概念。
数的范围是在数的当前表示格式下,最小的值到最大值中间的间隔。比如16位有符号的整数范围是-32768 to 32767,超过范围的就不能表示,需要注意的是,在范围内的有些数,在当前格式下也不能表示,比如整型就没法表示小数。所以每种类型都只能表达实数轴上离散的点。
精度和准确性通常让人混淆,这是两个完全不同的概念。精度是“表示数的格式”的一种属性,1.3333精确到小数点后四位,1.333300精确到小数点后6位,但他们是同一个值,但是1.333300的下一个数是1.333301,1.3333的下一个数是1.3334,可见高精度可以表示同一个范围内更多的数。
精度会影响到运算结果,所以在系统中需要特别注意这些问题:
Using one digit precision:
0.4 * 0.6 + 0.6 * 0.4 = 0.24 + 0.24 Calculate products
= 0.2 + 0.2 Round to 1 digit
= 0.4 Final result
Using two digit precision:
0.4 * 0.6 + 0.6 * 0.4 = 0.24 + 0.24 Calculate products
= 0.24 + 0.24 Keep the 2 digits
= 0.48 Calculate sum
= 0.5 Round to 1 digit
准确性需要在使用的上下文中讨论,表示运算的结果和真实的值之间的差异的大小,准确性和错误相关,高准确性意味着小的误差。准确性和精度是相关的,但是并不是直接相关,低精度在有些场景中,也是准确的,比如
Byte n0 = 0x03;
Int16 n1 = 0x0003;
Int32 n2 = 0x00000003;
Single n3 = 3.000000f;
Double n4 = 3.000000000000000;
每种格式都准确表达了3,但是这些变量的精度是不同的,使用的位数也不一样,从8到64位。
假定你要在系统中使用pi,你会发现不管用任何类型的变量,你都无法准确表达它,你的计算必然包含着误差,你只能选择一个近似于pi的数去运算。你使用的不是pi,而是pi+e,假定e为舍入误差。
更不爽的是,你的给个运算都会带来新的误差,比如你需要计算(a+b),假定a,b和pi一样是无理数,那么实际上你计算的是(a + b) + (Ea + Eb + Esum)。
我们必须找到一种方法减少误差,使我们的结果可以反馈出实际的情况。可以看两个因为计算不准确导致的事故
1990年2月25日,海湾战争期间,在沙特阿拉伯宰赫兰的爱国者导弹防御系统因浮点数舍入错误而失效,该系统的计算机精度仅有24位,存在0.0001%的计时误差,所以有效时间阙值是20个小时。当系统运行100个小时以后,已经积累了0.3422秒的误差。这个错误导致导弹系统不断地自我循环,而不能正确地瞄准目标。结果未能拦截一枚伊拉克飞毛腿导弹,飞毛腿导弹在军营中爆炸,造成28名美国陆军死亡。
1996年6月4日,在亚利安五号运载火箭发射后37秒,偏离预定轨道而炸毁。原因是软件系统试图将64位浮点数转换为16位浮点数,造成溢出错误。
温哥华证券交易所曾开发了一项股票指数。当其在1982年推出时,指数的值是1000.000。在后来的重新计算时多次运用舍入到小数点后三位的操作。22个月以后,指数的值是524.881,然而事实上应该是1009.811。
运算 | 大小 |
---|---|
long.max | 9223372036854775807 |
1亿*1亿 | 10000000000000000 |
现在虽然已经给Neo选择好类型,但是我们进入代码,会发现Neo使用了一个叫Fixed8的类型表示数据,所以我们进一步讨论。
简单的来说,定点数就是小数点在特定位置的数,比如Neo中的fixed8就是始终保持小数点后8位。
定义一个定点数,需要包含两部分:
定点数实际上使用整型表达就可以了,我们只需要另外保存一下精度,比如Fixed8就是把long型除以100000000。下图显示了定点数的格式
在看源码的时候,发现fixed8的乘法是使用位移方法自己实现的,结果发现其准确性并没有两个decimal相乘高,虽然如此小的误差不一定会给系统带来什么影响,但是还是一个有趣的发现。
https://github.com/neo-project/neo/issues/194
如果数字的范围和精度都极大,那么就需要特殊处理,Neo中使用了BigInteger封装了一个BigDecimal的结构体。BigInteger二的地方是不支持小数,所以Neo中的小数必须乘以一个精度转成BigDecimal,BigDecimal只是保存了一下精度。
在Neo中发现还有两个基于byte的数据类型UInt256,UInt160。这两个主要用来保存hash算法的的值。
UInt256有32的字节大小,256个bit,用来保存"22c199231f06e2091f89e0128e7d875fdfb0fb5fc7c4f916af0c50d04ab70e74"
这样的字符串。这个字符串是64个字符组成的,而64位机器上,char类型是2个字节,为什么用32位就可以表示呢?我们看看下面代码。
public static byte[] HexToBytes(this string value)
{
if (value == null || value.Length == 0)
return new byte[0];
if (value.Length % 2 == 1)
throw new FormatException();
byte[] result = new byte[value.Length / 2];
for (int i = 0; i < result.Length; i++)
result[i] = byte.Parse(value.Substring(i * 2, 2), NumberStyles.AllowHexSpecifier);
return result;
}
这段代码表示把以字符串表示的的16进制的数转成byte数组。所以只要hash的目标集合在0~f之间就可以。
我们可以看到"22c199231f06e2091f89e0128e7d875fdfb0fb5fc7c4f916af0c50d04ab70e74"
就在这个范围,如果你改一下,比如加个z
进去,就会抛出异常了。
上面只是简单的介绍,基本上是很容易理解的,但是实际上数的表示和运算是一个非常基础的问题,越简单的问题往往包含着很复杂的原理,下面的参考文档有很多有意思的细节,建议阅读。
neo白皮书
Floating Point in .NET part 1: Concepts and Formats
Introduction to Fixed Point Math for Embedded Systems - Part 1 of 3
Fixed Point Arithmetic 1: Intro to Fixed Point
Fixed Point and Floating Point binary numbers
fixed point representation in computer organization| COA
Floating Point/Fixed-Point Numbers