计算机中的数字
http://nozer0.github.io/zh/technology/system/numbers-in-computer/这次,我们来看一下数字在计算机中是如何存储的。
在开始前,让我们先来看一个简单程序。
#include <stdio.h>
int main(int argc, char* argv[]) {
int i = 1;
float *p2 = (float *)&i;
printf("%.20g\n", *p2);
printf("%d\n", 0.4 + 0.2 == 0.6);
return 0;
}
有些意外?
进制
我想对于进制不需要更多解释了,列举一些计算机中经常涉及的进制。
Binary | Octal | Decimal | Hexadecimal |
---|---|---|---|
10011101 | 235 | 157 | 9D |
0.100111 | 0.47 | 0.609375 | 0.9C |
如何在不同进制间转换?看个例子。
Q: 26(D) = ?(B) A: 26 | 13 | 6 | 3 | 1 / 2 remainder 0 | 1 | 0 | 1 | 1 26(D) = 11010(B) `````` Q: 0.8125(D) = ?(B) A: fractional 0.8125 | 0.625 | 0.25 | 0.5 | 0 * 2 integer 1 | 1 | 0 | 1 0.8125(D) = 0.1101(B)
数字码
计算机天然地使用二进制来表示数字,或者说,是用一系列的bit位来表示数字,通常用32位存储整数。
直接用数字的二进制表示再加上一bit表示符号位,0表示正数,1表示负数,这种方式被称为‘原码(original code)’表示法。原码最容易理解,但需要根据符号位不同来决定做加法还是减法,而且0有两种格式,‘0 0..0’以及‘1 0..0’。
为了简便计算,人们定义了另一种称为‘反码(anti code)’或‘一补码’的表示法,如果是负数,就将原码除符号位以外的所有bit反转,否则,与原码相同。符号位也可以直接参加运算,同时,可以用加法代替减法运算,比如。
1 1110111 (-8) 0 0001000 (+8) + 0 0001101 (+13) + 0 0001101 (+13) ------------------ ------------------ 1 0 0000100 0 0 0010101 + 1 (carry) + 0 (carry) ------------------ ------------------ 0 0000101 (+5) 0 0010101 (+21)
不过,反码还是存在两种0的格式,+0的‘0 0..0’,还有-0的‘1 1..1’。‘二补码’,简称‘补码(complement code)’,成了最后的赢家,成为目前整数的标准表示。区别就是,负数表示为反码的负数再加1,其余相同,‘1 1..1’变为-1,而不是-0,相比反码,可以多表示一个‘-2^(bit-1)’的数字,也不需要最后再加上进位的步骤。
实际上,-8的补码111111000 = 244 和+8的补码000001000 = 8对于模256同余,这就是为什么可以只用简单的加法器和补码器来实现减法的原因。
1 1111000 (-8) 0 0001000 (+8) + 0 0001101 (+13) + 0 0001101 (+13) ------------------ ------------------ 1 0 0000101 (+5) 0 0 0010101 (+21)
‘移码(shift code, bias code)’, 在下面介绍到的IEEE.754标准中有应用,是给数字本身的二进制值加上2^(bit-1)的位移来避免负数形式。
正数(38) | 负数(-38) | 定义 | 优缺点 | |
---|---|---|---|---|
原码 | 0 0100110 | 1 0100110 | 符号 + 二进制 | 易理解,符号位不能参加运算 |
反码 | 0 0100110 | 1 1011001 | X > 0 ? : ~b | 符号位可加入运算,0有两种格式 |
补码 | 0 0100110 | 1 1011010 | anti + 1 | +0和-0统一表示,对32位来说可以表示128 |
移码 | 1 0100110 | 0 1011010 | X + 2^(k-1) | 大小比较很方便,0有唯一的格式 |
范围 | UInteger | Integer | Fixed Fraction |
---|---|---|---|
原码 | 0 ~ 2^32-1 | 1-2^31 ~ 2^31-1 | -1+2^(-31) ~ 1-2^(-31) |
反码 | 1-2^31 ~ 2^31-1 | -1+2^(-31) ~ 1-2^(-31) | |
补码 | -2^31 ~ 2^31-1 | -1 ~ 1-2^(-31) | |
移码 | -2^31 ~ 2^31-1 |
字节序
我们已经知道了不同的数字码表示,但当存储到字节中时,根据字节顺序的不同,还有两种不同的格式。如果把最高有效数字放在地址最低的字节,则成为‘大头(big-endian)’排序,反之,则是‘小头(little-endian)’排序,名称来源于《格列佛游记》一书。 例如,数字‘0xabcd’,如果是大头排序的话,存储的字节从低到高是从‘a’到‘d’,反之则是从‘d’到‘a’。目前绝大多数的CPU,像Intel、AMD都是使用小头排序。
浮点数
我们已经知道计算机如何用补码来存储整数,那小数呢?最简单的方法就是预先规定小数点的位置,因此称为‘定点’表示法,比如,如果我们定义小数点在第三bit,即0~2bit表示小数部分,则0 0010 110 = 2.75。
定点数表示法相对简单,但是可表示数字的范围比相同bit位的整数还要小,为了能表示更多的数字以及更大的范围,通常都用‘浮点’表示法来表示小数,也就是说,小数点是浮动的。
规格化
一个数字可以有无数的表示形式,例如,100110可以表示为110.11 * 2^3
,或者1.1011 * 2^5
,将数字转化为整数部分只有一位数字的表示方法,称为‘规格化(Normalization)’。
与‘规格化’相反,‘非规格化(Denormalization)’没有指数部分。
IEEE.754
所有现代的计算机都遵循IEEE 754标准来存储浮点数。格式表示如下。
类型 | 位 | 符号 | 指数 | 小数 |
---|---|---|---|---|
float | 32 | 1 | 8 | 23 |
double | 64 | 1 | 11 | 52 |
由上表可见,浮点数由三部分组成,符号位(sign),指数部分(exponent)和小数部分(fraction),我们将使用小写字母‘s’,‘e’和‘f’来表示各个部分的bit长度,‘S’,‘E’和‘F’来表示值。
指数部分用移码表示,指数 = E - bias = E - (2^(e-1) - 1)
,对于32位,位移 = 2^(8-1) - 1 = 127,指数 = -127 ~ 128。
为表示更多的精度,同时应用了规格化及非规格化的形式。
规格化的数字可以有更多的有效位,而且,所有规格化数字整数部分都是1,因此可以默认省略,可得出公式float = -1^S * 1.F * 2^(E - (2^(e-1) - 1))
。
以32位为例子,如果我们只使用规格化数字,比0大的最小数字绝对值是1.0..00 * 2^-127,次之是1.0..01 * 2^-127,最小绝对值和0之间的空距比之与次小值的差距2^-(127+23) = 2^-150大的多。更直观的可以看下图。
┌─── 2^-149 ───┐ ┌ 2^-150 ┐ ┌ 2^-150 ┐ ┌──────────── 2^-127 ────────────┐
1.0..1*2^-126, 1.0..0*2^-126, 1.1..1*2^-127, ..., 1.0..1*2^-127, 1.0..0*2^-127, 0
这被称为‘突然下溢’,因此对于绝对值小于2^(e-1) - 2的数字,IEEE使用了非规格化的表示方式,并且指数的位移部分+1,来实现所谓的‘渐进下溢’。
┌─── 2^-148 ───┐ ┌ 2^-149 ┐ ┌ 2^-149 ┐ ┌ 2^-149 ┐
1.0..1*2^-125, 1.0..0*2^-125, ..., 1.0..1*2^-126, 1.0..0*2^-126, 0.1..1*2^-126, ..., 0.0..1*2^-126, 0
IEEE 754浮点表示的结构表。
指数 | E | 小数 | 32 bits | |
---|---|---|---|---|
0 | 00..0 | 2 - 2^(e-1) | [0.]00..0 | 0 |
Min Denormalized N | 00..0 | 2 - 2^(e-1) | [0.]00..1 | 2^-149 |
Denormalized N | 00..0 | 2 - 2^(e-1) | [0.]xx..x | 0.F * 2^-126 |
Max Denormalized N | 00..0 | 2 - 2^(e-1) | [0.]11..1 | 1 - 2^-149 |
Min Normalized N | 00..1 | 2 - 2^(e-1) | [1.]00..0 | 2^-126 |
Normalized N | xx..x | E + 1 - 2^(e-1) | [1.]xx..x | 1.F * 2^(E - 127) |
Max Normalized N | 11..0 | 2^(e-1) - 1 | [1.]11..1 | 2^128-2^104 |
Infinity | 11..1 | 1 - 2^(e-1) | [1.]00..0 | |
NaN | 11..1 | 1 - 2^(e-1) |
现在,让我们回头看看开头的例子。
综上,我们可以得出,1的整数字节形式为‘00000000 00000000 00000000 00000001’,同样的字节在浮点数中表示2^-149。
0.2 + 0.4的结果表示为字节‘00111111 00011001 10011001 10011010’,实际上,这就是0.6的浮点数表示,问题出在精度上,事实上,0.6的二进制表示是一个无限循环小数,‘0.100[1100]*’,是不可能被计算机精确表示的,根据不同精度,‘00000000 00000000 00000000 00000001’表示的结果可能是0.6000000000000001或0.60000000000000008,0.2和0.4同理。
可以用这个小工具来转换不同进制的数字及数字码表示。