深入理解计算机系统(5)_计算机运算

文章详细介绍了计算机系统的基础知识,包括二进制编码、浮点数表示及其不精确性,以及处理器和存储器的设计。重点讨论了浮点数的科学计数法表示,如单精度和双精度浮点数,并分析了浮点数加法时可能出现的精度损失。此外,还提到了KahanSummation算法作为解决浮点数精度问题的一种方法。
摘要由CSDN通过智能技术生成

深入理解计算机系统系列文章目录

第一章 计算机的基本组成
1. 内容概述
2. 计算机基本组成

第二章 计算机的指令和运算
3. 计算机指令
4. 程序的机器级表示
5. 计算机运算
6. 信息表示与处理

第三章 处理器设计
7. CPU
8. 其他处理器

第四章 存储器和IO系统
9. 存储器的层次结构
10. 存储器和I/O系统



前言


参考资料

《深入理解计算机系统》
《深入浅出计算机组成原理》


一、二进制编码

1. 二进制

一个 4 位的二进制数, 0011 就表示为 +3。
而 1011 最左侧的第一位是 1,所以它就表示 -3。
这个其实就是整数的原码表示法。
原码表示法有一个很直观的缺点就是,0 可以用两个不同的编码来表示,1000 代表 0, 0000 也代表 0。

我们仍然通过最左侧第一位的 0 和 1,来判断这个数的正负。
但是,我们不再把这一位当成单独的符号位,在剩下几位计算出的十进制前加上正负号,
而是在计算整个二进制值的时候,在左侧最高位前面加个负号。
比如,一个 4 位的二进制补码数值 1011,转换成十进制,就是 −1×23+0×22+1×21+1×20 =−5。
如果最高位是 1,这个数必然是负数;最高位是 0,必然是正数。
并且,只有 0000 表示 0,1000 在这样的情况下表示 -8。
一个 4 位的二进制数,可以表示从 -8 到 7 这 16 个整数,不会白白浪费一位。
当然更重要的一点是,用补码来表示负数,使得我们的整数相加变得很容易,不需要做任何特殊处理,
只是把它当成普通的二进制相加,就能得到正确的结果。
在这里插入图片描述

2. 字符串

不仅数值可以用二进制表示,字符乃至更多的信息都能用二进制表示。
最典型的例子就是字符串(Character String)。
最早计算机只需要使用英文字符,加上数字和一些特殊符号,然后用 8 位的二进制,就能表示我们日常需要的所有字符了,
这个就是我们常常说的 ASCII 码(American Standard Code for Information Interchange,美国信息交换标准代码)。

在 ASCII 码里面,数字 9 不再像整数表示法里一样,用 0000 1001 来表示,而是用 0011 1001 (0039)来表示。
字符串 15 也不是用 0000 1111 这 8 位来表示,
而是变成两个字符 1 和 5 连续放在一起,也就是 0011 0001 和 0011 0101,需要用两个 8 位来表示。

我们可以看到,最大的 32 位整数,就是 2147483647。
如果用整数表示法,只需要 32 位就能表示了。
但是如果用字符串来表示,一共有 10 个字符,每个字符用 8 位的话,需要整整 80 位。
比起整数表示法,要多占很多空间。

这也是为什么,很多时候我们在存储数据的时候,要采用二进制序列化这样的方式,
而不是简单地把数据通过 CSV 或者 JSON,这样的文本格式存储来进行序列化。
不管是整数也好,浮点数也好,采用二进制序列化会比存储文本省下不少空间。

ASCII 码只表示了 128 个字符,一开始倒也堪用,毕竟计算机是在美国发明的。
然而随着越来越多的不同国家的人都用上了计算机,想要表示譬如中文这样的文字,128 个字符显然是不太够用的。
于是,计算机工程师们开始各显神通,给自己国家的语言创建了对应的字符集(Charset)和字符编码(Character Encoding)。

而字符编码则是对于字符集里的这些字符,怎么一一用二进制表示出来的一个字典。
我们上面说的 Unicode,就可以用 UTF-8、UTF-16,乃至 UTF-32 来进行编码,存储成二进制。
只要别人知道这套编码规则,就可以正常传输、显示这段代码。

同样的文本,采用不同的编码存储下来。
如果另外一个程序,用一种不同的编码方式来进行解码和展示,就会出现乱码。
这就好像两个军队用密语通信,如果用错了密码本,那看到的消息就会不知所云

二、浮点数

1. 浮点数的不精确性

32 个比特,只能表示 2 的 32 次方个不同的数,差不多是 40 亿个。
如果表示的数要超过这个数,就会有两个不同的数的二进制表示是一样的。
那计算机可就会一筹莫展,不知道这个数到底是多少。

2. 定点数BCD

有一个很直观的想法,就是我们用 4 个比特来表示 0~9 的整数(0000 ~ 1001),那么 32 个比特就可以表示 8 个这样的整数。
然后我们把最右边的 2 个 0~9 的整数,当成小数部分;把左边 6 个 0~9 的整数,当成整数部分。
这样,我们就可以用 32 个比特,来表示从 0 到 999999.99 这样 1 亿个实数了。

这种用二进制来表示十进制的编码方式,叫作BCD 编码(Binary-Coded Decimal)。
BCD 编码的实数,就是小数点固定在某一位的方式,我们也就把它称为定点数。
其实它的运用非常广泛,最常用的是在超市、银行这样需要用小数记录金额的情况里。
在超市里面,我们的小数最多也就到分。
这样的表示方式,比较直观清楚,也满足了小数部分的计算。

不过,这样的表示方式也有几个缺点。

  1. 这样的表示方式有点“浪费”。
    本来 32 个比特我们可以表示 40 亿个不同的数,
    但是在 BCD 编码下,只能表示 1 亿个数,
    如果我们要精确到分的话,那么能够表示的最大金额也就是到 100 万。
  2. 这样的表示方式没办法同时表示很大的数字和很小的数字。
    我们在写程序的时候,实数的用途可能是多种多样的。
    有时候我们想要表示商品的金额,关心的是 9.99 这样小的数字;
    有时候,我们又要进行物理学的运算,需要表示光速,也就是 3×108 这样很大的数字。

3. 浮点数的表示

既能够表示很小的数,又能表示很大的数

在计算机里,我们也可以用一样的办法,用科学计数法来表示实数。
浮点数的科学计数法的表示,有一个 IEEE 的标准,它定义了两个基本的格式。
一个是用 32 比特表示单精度的浮点数,也就是我们常常说的 float 或者 float32 类型。
另外一个是用 64 比特表示双精度的浮点数,也就是我们平时说的 double 或者 float64 类型。
在这里插入图片描述

单精度的 32 个比特可以分成三部分。

  1. 一个符号位,用来表示是正数还是负数。
    我们一般用 s 来表示。
    在浮点数里,我们不像正数分符号数还是无符号数,所有的浮点数都是有符号的。
  2. 一个 8 个比特组成的指数位。
    我们一般用 e 来表示。
    8 个比特能够表示的整数空间,就是 0~255。
    我们在这里用 1~254 映射到 -126~127 这 254 个有正有负的数上。
    因为我们的浮点数,不仅仅想要表示很大的数,还希望能够表示很小的数,所以指数位也会有负数。
  3. 一个 23 个比特组成的有效数位。
    我们用 f 来表示。

综合科学计数法,我们的浮点数就可以表示成下面这样:

	(−1)s × 1.f × 2e

你会发现,这里的浮点数,没有办法表示 0。
的确,要表示 0 和一些特殊的数,我们就要用上在 e 里面留下的 0 和 255 这两个表示,这两个表示其实是两个标记位。
在 e 为 0 且 f 为 0 的时候,我们就把这个浮点数认为是 0。
至于其它的 e 是 0 或者 255 的特殊情况,你可以看下面这个表格,分别可以表示出无穷大、无穷小、NAN 以及一个特殊的不规范数。

在这里插入图片描述

0.5 = (−1)0 × 1.0 × 2−1 = 0.5,对应的浮点数表示,就是 32 个比特。
其中只有 0.5 能够被精确地表示成二进制的浮点数,也就是 s = 0、e = -1、f = 0 这样的情况。
而 0.3、0.6 乃至我们希望的 0.9,都只是一个近似的表达。
在这里插入图片描述

需要注意,e 表示从 -126 到 127 个,-1 是其中的第 126 个数,
这里的 e 如果用整数表示,就是 26+25+24+23+22+21=126,1.f=1.0。
在这样的浮点数表示下,不考虑符号的话,浮点数能够表示的最小的数和最大的数,
差不多是 1.17×10−38 和 3.40×1038。
比前面的 BCD 编码能够表示的范围大多了。

4. 浮点数的二进制转化

我们输入一个任意的十进制浮点数,背后都会对应一个二进制表示。
比方说,我们输入了一个十进制浮点数 9.1。
那么按照之前的讲解,在二进制里面,我们应该把它变成一个“符号位 s+ 指数位 e+ 有效位数 f”的组合。

  1. 转为二进制。
    首先,我们把这个数的整数部分,变成一个二进制。
    这里的 9,换算之后就是 1001。
    接着,我们把对应的小数部分也换算成二进制。
    我们拿 0.1001 这样一个二进制小数来举例说明。
    和上面的整数相反,我们把小数点后的每一位,都表示对应的 2 的 -N 次方。
    那么 0.1001,转化成十进制就是:

    	1×2−1+0×2−2+0×2−3+1×2−4=0.5625
    

    和整数的二进制表示采用“除以 2,然后看余数”的方式相比,
    小数部分转换成二进制是用一个相似的反方向操作,就是乘以 2,然后看看是否超过 1
    如果超过 1,我们就记下 1,并把结果减去 1,进一步循环操作
    在这里,我们就会看到,0.1 其实变成了一个无限循环的二进制小数,0.000110011。
    这里的“0011”会无限循环下去。
    在这里插入图片描述

  2. 将整数部分和小数部分拼接
    9.1 这个十进制数就变成了 1001.000110011…这样一个二进制表示。
    浮点数其实是用二进制的科学计数法来表示的,
    所以我们可以把小数点左移三位,这个数就变成了:

    		1.001000110011…×23
    

    那这个二进制的科学计数法表示,我们就可以对应到了浮点数的格式里了。
    这里的符号位 s = 0,对应的有效位 f=001000110011…。
    因为 f 最长只有 23 位,那这里“0011”无限循环,最多到 23 位就截止了。
    于是,f=00100011001100110011 001。
    最后的一个“0011”循环中的最后一个“1”会被截断掉。
    对应的指数为 e,代表的应该是 3。
    因为指数位有正又有负,所以指数位在 127 之前代表负数,之后代表正数,
    那 3 其实对应的是加上 127 的偏移量 130,转化成二进制,就是 130,
    对应的就是指数位的二进制,表示出来就是 10000010。

    在这里插入图片描述
    然后,我们把“s+e+f”拼在一起,就可以得到浮点数 9.1 的二进制表示了。最终得到的二进制表示就变成了:
    010000010 0010 0011001100110011 001

如果我们再把这个浮点数表示换算成十进制, 实际准确的值是 9.09999942779541015625。
相信你现在应该不会感觉奇怪了。

这个也解释了为什么,在上一讲一开始,0.3+0.6=0.899999。
因为 0.3 转化成浮点数之后,和这里的 9.1 一样,并不是精确的 0.3 了,0.6 和 0.9 也是一样的,最后的计算会出现精度问题。

5. 浮点数的加法和精度损失

先对齐、再计算。
两个浮点数的指数位可能是不一样的,
所以我们要把两个的指数位,变成一样的,
然后只去计算有效位的加法就好了。

比如 0.5,表示成浮点数,对应的指数位是 -1,有效位是 00…(后面全是 0,记住 f 前默认有一个 1)。
0.125 表示成浮点数,对应的指数位是 -3,有效位也还是 00…(后面全是 0,记住 f 前默认有一个 1)。

那我们在计算 0.5+0.125 的浮点数运算的时候,
首先要把两个的指数位对齐,也就是把指数位都统一成两个其中较大的 -1。
对应的有效位 1.00…也要对应右移两位,因为 f 前面有一个默认的 1,所以就会变成 0.01。
然后我们计算两者相加的有效位 1.f,就变成了有效位 1.01,
而指数位是 -1,这样就得到了我们想要的加法后的结果。
在这里插入图片描述
回到浮点数的加法过程,你会发现,
其中指数位较小的数,需要在有效位进行右移,在右移的过程中,最右侧的有效位就被丢弃掉了。
这会导致对应的指数位较小的数,在加法发生之前,就丢失精度。
两个相加数的指数位差的越大,位移的位数越大,可能丢失的精度也就越大。
当然,也有可能你的运气非常好,右移丢失的有效位都是 0。
这种情况下,对应的加法虽然丢失了需要加的数字的精度,
但是因为对应的值都是 0,实际的加法的数值结果不会有精度损失。

32 位浮点数的有效位长度一共只有 23 位,
如果两个数的指数位差出 23 位,较小的数右移 24 位之后,所有的有效位就都丢失了。
这也就意味着,虽然浮点数可以表示上到 3.40×1038,下到 1.17×10−38 这样的数值范围。
但是在实际计算的时候,只要两个数,差出 224,也就是差不多 1600 万倍,
那这两个数相加之后,结果完全不会变化。

让一个值为 2000 万的 32 位浮点数和 1 相加,
你会发现,+1 这个过程因为精度损失,被“完全抛弃”了。

在这里插入图片描述

6. Kahan Summation 算法

那么,我们有没有什么办法来解决这个精度丢失问题呢?
虽然我们在计算浮点数的时候,常常可以容忍一定的精度损失,
但是像上面那样,如果我们连续加 2000 万个 1,2000 万的数值都会被精度损失丢掉了,就会影响我们的计算结果。

一个常见的应用场景是,在一些“积少成多”的计算过程中,
比如在机器学习中,我们经常要计算海量样本计算出来的梯度或者 loss,于是会出现几亿个浮点数的相加。

每个浮点数可能都差不多大,但是随着累积值的越来越大,就会出现“大数吃小数”的情况。
我们可以做一个简单的实验,用一个循环相加 2000 万个 1.0f,最终的结果会是 1600 万左右,而不是 2000 万。
这是因为,加到 1600 万之后的加法因为精度丢失都没有了。
这个代码比起上面的使用 2000 万来加 1.0 更具有现实意义。

在这里插入图片描述

面对这个问题,聪明的计算机科学家们也想出了具体的解决办法。
他们发明了一种叫作Kahan Summation的算法来解决这个问题。
从中你可以看到,同样是 2000 万个 1.0f 相加,用这种算法我们得到了准确的 2000 万的结果。

在这里插入图片描述

其实这个算法的原理其实并不复杂,就是在每次的计算过程中,都用一次减法,把当前加法计算中损失的精度记录下来,
然后在后面的循环中,把这个精度损失放在要加的小数上,再做一次运算。

这个方法在实际的数值计算中也是常用的,也是大量数据累加中,解决浮点数精度带来的“大数吃小数”问题的必备方案。


数字电路

1. 电路原理

  1. 信号
    在这里插入图片描述
  2. 继电器
    在这里插入图片描述
  3. 组合逻辑
    在这里插入图片描述

2. 加法器

在这里插入图片描述

  1. 异或门和半加器
    基础门电路,输入都是两个单独的 bit,输出是一个单独的 bit
    如果我们要对 2 个 8 位(bit)的数,计算与、或、非这样的简单逻辑运算,其实很容易。
    只要连续摆放 8 个开关,来代表一个 8 位数。
    这样的两组开关,从左到右,上下单个的位开关之间,
    都统一用 “与门” 或者 “或门” 连起来,就是两个 8 位数的 AND 或者 OR 的运算了。

    先回归一个最简单的 8 位的无符号整数的加法。
    这里的“无符号”,表示我们并不需要使用补码来表示负数。
    无论高位是“0”还是“1”,这个整数都是一个正数。

    要表示一个 8 位数的整数,简单地用 8 个 bit,也就是 8 个像上一讲的电路开关就好了。
    那 2 个 8 位整数的加法,就是 2 排 8 个开关。
    加法得到的结果也是一个 8 位的整数,所以又需要 1 排 8 位的开关。
    要想实现加法,我们就要看一下,通过什么样的门电路,能够连接起加数和被加数,得到最后期望的和。
    在这里插入图片描述

    一方面,我们需要知道,加法计算之后的个位是什么,

    • 在输入的两位是 00 和 11 的情况下,对应的输出都应该是 0;
    • 在输入的两位是 10 和 01 的情况下,输出都是 1。
      结果你会发现,这个输入和输出的对应关系,其实就是我在上一讲留给你的思考题里面的“异或门(XOR)”。

    异或门就是一个最简单的整数加法,所需要使用的基本门电路。

    输入的两位都是 11 的时候,我们还需要向更左侧的一位进行进位。
    那这个就对应一个与门,也就是有且只有在加数和被加数都是 1 的时候,我们的进位才会是 1。

    所以,通过一个异或门计算出个位,通过一个与门计算出是否进位,我们就通过电路算出了一个一位数的加法。
    于是,我们把两个门电路打包,给它取一个名字,就叫作半加器(Half Adder)。
    在这里插入图片描述

3. 乘法器


总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值