“本节内容节选自下方 参考资料 1
在讨论「二进制」和「CPU 如何工作」之前,我们先来讨论一下我们生活中最稀疏平常的 数字,我们与之频繁地打交道:一个约定的时间、一件商品的价格、一个人的身高....却很少有人细细想过,这些数字是如何表达出来的?为什么你理所当然地把 1024
理解为「一千零二十四」而不是别的含义?
也许你从未想过,在这简单的记数中,沉淀着人类的大智慧。
图片来源:https://www.goethe.de/ins/cn/cn/kul/mag/20629923.html
早在数字的概念产生之前,人类就学会了使用树枝、石子、贝壳等自然界随处可见的小物件表示猎物的、果实的、部落人口的数量。比如在某个角落堆上一堆石子,每打到 1 只猎物,就扔 1 颗石子进去,每吃掉 2 只猎物,就从中取走 2 颗石子。他们并不在意石子的总数,只是时不时地瞅一眼,心底大致有数。
其实这是一种最朴素的记数方式,数学家称之为 一进制记数法(unary numeral system)。我们把它符号化一下,比如用斜杠 /
来表示:
1
就是 /
;
2
就是 //
;
4
就是 ;
好像没毛病,我们平时掰手指用的就是这种记数法,但数字一大,场面就要失控了。
为了解决记录大数的问题,于是我们得发明一些其他符号来表示更大的数值,比如用横杠 -
表示 10
,用十字 +
表示 100
。那么:
16
就是 -//
;
32
就是 ---//
;
128
就是 +--
;
漂亮....这种靠符号类型和符号数量表示数字的方法被称为 符值相加记数法(sign-value notation),古埃及和古罗马用的都是它,只不过符号各不相同。
古埃及的记数符号:
1 | 10 | 100 | 1000 | 10000 | 100000 | 1000000 |
---|---|---|---|---|---|---|
1024
在古埃及就写作:
你会发现,符值相加记数法的一大优点是,符号的顺序可以任意打乱,数字含义不受影响。我国藏族曾用石子表示 1
、木棍表示 10
、果核表示 100
、蚕豆表示 1000
、瓦片表示 10000
,那么,当你把 1
颗蚕豆、2
根木棍和 4
颗石子胡乱地攥在手里,别人依然知道它们是 1024
。
古罗马的做法略有不同,他们对五进制情有独钟:
1 | 5 | 10 | 50 | 100 | 500 | 1000 |
---|---|---|---|---|---|---|
I | V | X | L | C | D | M |
这些符号沿用至今,想必大家(至少对前 3
个)都比较熟悉,许多钟表仍保留着使用罗马数字的习惯,1~12
分别表示为:I
、II
、III
、IV
、V
、VI
、VII
、VIII
、IX
、X
、XI
、XII
。你会发现,罗马记数法是符值相加记数法的变种,因为它不光「相加」,还「相减」。这种方式就不允许符号乱序了,IV
和 VI
表示的是不同的数字。
那罗马人何苦要使用这种更复杂的记数法呢?无非是为了读写方便。同样表示 9
,IX
比 VIIII
更简洁。
其实有一种更好使的方法——用另外一些列符号来表示符号的数量。比如用 A
表示 1
个符号,用 B
表示 2
个符号,以此类推,用 I
表示 9
个符号。
如此,上文表示 256
的 ++-----//
就可以写作 B+E-F/
。你一定感觉莫名其妙,这种写法哪里方便了。其实中文的数字表示就是这种形式,只不过我们用得太习惯了,以至于没有发现。
在中文中,个
、十
、百
代替了 /
、-
、+
,而 一
、二
、三
代替了 A
、B
、C
。256
就写作 二百五十六个
,个
比较累赘,我们通常把它省略了。
其实像日语、英语用的也同样是这种记数法,简洁、优雅。
美中不足的是,这种形式虽便于读写,却不便于计算。中国古人为算筹和算盘这类经典算具搭建起广阔的舞台,却没给笔算留出一席之地。想象一下,如果让你把这些汉字写在草稿纸上,列个竖式,你的内心一定非常别扭。
公元5世纪,印度数学家阿耶波多(Aryabhata 476–550)创立了现在广泛使用的 位值制记数法(positional notation/place-value notation),该记数法使用的主要符号,是同为印度人发明的阿拉伯数字:0
、1
、2
、3
、4
、5
、6
、7
、8
、9
。
与符值相加记数法类比,位值制中的 1
、2
、3
代替的是 A
、B
、C
,那 /
、-
、+
呢?是 靠阿拉伯数字的位置来表示的。众所周知,最右位相当于 /
,次右位相当于 -
。靠每个位置上的数值来表示数字,故名位值制。
严谨的数学家用一种多项式高度概括了位值制记数法的本质,在十进制中,这个多项式是这样的:
这是一个 n
位十进制数,ai 就是第 i 位上的数值。为便于直观理解,举个 1024
的例子吧:
由于我们熟悉了十进制,这样费心费力的展开可能会让你觉得好笑,但当我们把它推广到其他进制时,这个多项式的价值就体现了出来。n 位 b 进制数的位值制表示:
1024
用二进制怎么表示?
因此,1024
的二进制写作 10000000000
。
除了最普遍的十进制和计算机中的二进制,常见的还有七进制(如 1
周 7
天)、十二进制(如 1
年 12
个月)、十六进制(如古代 1
斤 16
两)、六十进制(如六十甲子)等等,只要有意义,任何进制都可以为你所用。
在上述的多项式中,如果 ai 或 b 的取值奇葩一点,就形成了 非标准位值制(non-standard positional numeral systems),这类记数法往往应用于专业领域,很难在日常生活中见到。比如标准位值制中的三进制 ai 的取值为 0
、1
、2
,但在一种名为平衡三进制(balanced ternary)的非标准位值制中,ai 取 -1
、0
、1
,苏联曾使用这种进制研发电子计算机。
图片来源:https://zhuanlan.zhihu.com/p/26743163
至此,你对「二进制」应该会感觉亲切了些,它只是一种数制而已,本质上与我们熟悉的十进制没有很大的差别,我们这一 Part 来稍微理解一下二进制。(至于电脑为什么使用二进制我们在下一 Part 中介绍)
十进制中的那些基本运算原则,二进制中同样适用,只不过需要稍加变幻而已,下面我们分别就加、减、乘、除四则运算来介绍。
根据「逢二进一」规则,二进制数加法的法则为:
0+0=0
0+1=1+0=1
1+1=0 (进位为1)
1+1+1=1 (进位为1)
例如:1101
和 1011
相加过程如下:
根据「借一有二」的规则,二进制数减法的法则为:
0-0=0
1-1=0
1-0=1
0-1=1 (借位为1)
例如:1101
减去 1011
的过程如下:
二进制数乘法过程可仿照十进制数乘法进行。但由于二进制数只有 0
或 1
两种可能的乘数位,导致二进制乘法更为简单。二进制数乘法的法则为:
0×0=0
0×1=1×0=0
1×1=1
例如:1001
和 1010
相乘的过程如下:
二进制数除法与十进制数除法很类似。
例如:100110
÷ 110
的过程如下:
因为编码规定。
之前我们有说到,所有保存的程序和数据在计算机中都被描述为 文件,也就是说我们能够知道当前的数据集合被期望的用途是什么,也就能够找到对应的 处理器 来正确处理当前的数据。
拿文字举例,为了让一串 0
、1
能够代表特定的文字,人们规定使用一个字节中的七位来表达特定的文字, 这就是大名鼎鼎 ASCII (American Standard Code for Information Interchange) 码,ASCll 码能够表达 27=128 种字符(编码从 0~127
),对于 26
个英文字母和一些常用的可打印字符,这完全足够了:
可是,世界文化是多元的,面对类似汉字这样的象形文字,ASCll码表用起来自然是捉襟见肘。
穷则思变,一个字节不行,那就两个字节,这就是大名鼎鼎的 Unicode 码,不难看出,Unicode 码有 216=65536 种表示方式,这样就足以表达一些常用的字符了,值得一提的是,Unicode 码算是在 ASCll 码上的一种扩充,其第 0~127
个编码字符与 ASCll 码表一模一样。
这里涉及一点点物理知识,话说很久以前,牛顿通过三棱镜把白色的光分解成七种不同颜色的光,后来又通过各种实验发现红、绿、蓝三种颜色的光是无法被分解的,因此我们就称为红蓝绿为光的三原色。
至此人类已经知道了:可以通过组合不同比例的红、绿、蓝三种颜色来获得各种各样的颜色,那么我们就可以在计算机上模拟了。现在的计算机,一般使用 32
位来表示颜色,32
位平分给四个分量,也就是每个分量 8
位。
为啥是四个颜色分量?
因为颜色模型中有一个 alpha 值,用来表示透明度,这一点我们先不考虑。总之三种颜色,每个使用 8
位来表示的话,我们就能够表示 256 * 256 * 256 = 16777216
种颜色了,已经足够基础的使用了。
先来看一张图片:
这张图像的尺寸是 600px * 664px
(px 是一种图片单位,中文名称为像素,你可以暂时理解为一个点)。我们把它放大一下,如下图所示:
看见了吗?实际上,大部分图像(你拍摄的照片、你扫描的图片、你使用 iPad 画的图片等等...)都是位图文件,位图就是由像素点构成的,它就像是一个网格一样,每个格子里面填一个颜色。(除了位图外,还有一种图是矢量图,它描述的是形状而非网格)
OK,我想你已经能理解图像是由像素点组成的了(事实上我们的显示器也是),我们只需要在编码中附带上一些额外的信息,例如图像有多大的尺寸、时间、作者、颜色深度、是否支持透明度之类的就能够对图像进行正确表示了。(视频可以简单理解成一张张连续不断的图片)
要让显示器正确显示图片或者视频,只需要让显示器上每个像素显示特定的颜色就好了。
图片来源:https://www.bbc.co.uk/bitesize/topics/zf2f9j6/articles/z2tgr82
图片来源:https://zhuanlan.zhihu.com/p/33439000
可为什么一定是二进制呢?使用人类习惯的十进制不好吗?
计算机依靠电力工作,这也就意味着需要将数字信号映射到电信号,实现这种映射最简单的方法是:
0 - 没有电(0 V)
1 - 有点(5 V)
二进制在技术上最容易实现。这是因为具有两种 稳定状态 的物理器件很多,如门电路的导通与截止、电压的高与低等,而它们恰好可以对应表示 “1” 和 “0” 这两个数码。假如采用十进制,那么就要制造具有 10
种 稳定状态 的物理电路,而这是非常困难的。
为什么使用更复杂的数字系统是一个问题?
假设我们使用三元(3 位数字)数字系统涉及计算机,如果我们具有从 0 V
到 5 V
的电压,那么我们可以进行以下的映射:
0 - 0 V;
1 - 2.5 V;
2 - 5 V;
图片来源:https://pmihaylov.com/intro-binary-numbers/
看起来合理吧?但是,想象一下,我以 2.5 V
的电压发送了一个数字。但是由于电路中的一些噪声,我在输出端得到 2.3 V
的电压,因此将其视为 0
。结果是?
有人给我发送了 1
,但我将其视为 0
。数据丢失可是一个非常严重的问题。
使用二进制则可靠得多,由于电压的高和低、电流的有和无等都是一种 质的变化,两种物理状态稳定、分明,因此,二进制码传输的抗干扰能力强,鉴别信息的可靠性高。
图片来源:http://programmedlessons.org/Java9/chap02/ch02_11.html
建立数字系统的目的是 仅在某些时间点测试开/关(二进制)值,这使电线(或其他设备)有时间更换。这就是计算机系统有时钟的原因。
时钟会周期性地进行信号的测量,图中所示的 T1 和 T2 就是可以测量信号的时间点。
时钟利用所有这些时间点来保持同步。更快的时钟意味着每秒可以对电线进行更多次测试,并且整个系统运行得更快。2 GHz
处理器每秒检查二进制值 20
亿次。在这些时间之间,允许值改变并稳定下来。处理器芯片速度越快,每秒可以测试的次数就越多,每秒可以做出的决策就越多。
数学推导已经证明,对 N
进制数进行算术求和或求积运算,其运算规则各有 N(N+1)/2
种。如采用十进制,则 N=10
,就有 55
种求和或求积的运算规则;而采用二进制,则 N=2
,仅有 3
种求和或求积的运算规则,以上面提到的加法为例:
0+0=0,0+1=1 (1+0=1),1+1=10
因而可以大大简化运算器等物理器件的设计。
采用二进制后,仅有的两个符号 “1” 和 “0” 正好可以与逻辑命题的两个值 “真” 和 “假” 相对应,能够方便地使用逻辑代数这一有力工具来分析和设计计算机的逻辑电路。
虽然在 1950 年代就造出了更加高效的三元计算机,但在效率和复杂度的取舍上,始终抵不过二进制。二进制仍然在当今世界中长期存在。
上面我们了解到计算机以二进制的形式运行,它们只有两种状态:开(1)和关(0),为了执行二进制计算,我们需要采用一种特殊的电子元器件,称为 「晶体管」。暂时我们把它理解为一种开关吧,通电就打开,没电流通过就关闭。
我们知道,给电灯通上电,它就会亮:
于是,结合上开关,我们就能搭建出最基础的 与门 和 或门。
该电路的逻辑是:只有当 A 和 B 同时开启时,LED 灯才会亮,也就是认为输出 1,我们可以利用电信号来简单模拟一下:
A | B | Y |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
该电路的逻辑是:当 A 或者 B 开启时,LED 灯就会亮,也就是认为输出 1,我们可以利用电信号来简单模拟一下:
A | B | Y |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
类似地,我们可以借助更多的电子元器件来创造出基础的 7
种逻辑门电路:
图片来源:https://www.zhihu.com/question/348237008/answer/843382847 | @Zign
这里需要特别提一下 异或门,我们需要先知道有一种电子元器件可以利用电气特性对 输入取反,也就是说输入 1
则输出 0
,输入 0
则输出 1
,那么我们就可以 简单模拟 出异或门逻辑电路(实际会更复杂些,这里仅展示出异或的意思):
A'
和 B'
分别表示 A
和 B
开关的反值,从图中我们很容易知道只有当 A
、B
只存在一个输入 1
时,整个电路才会输出 1
。
OK,上面我们了解到我们能够利用 "开关" 来模拟逻辑的运算,我们接下来试着还原一个简单的加法运算器是如何实现的:
仅需两个门,就可以完成基本的二进制加法运算。上图是利用 logic.ly
创建的半加法器,A
、B
相当于使我们计算的两个数,最后一块相当于是我们的数显芯片,它的功能是根据输入显示数字,从上到下的引脚(也就是图中输入的地方,通常我们这样称呼)分别对应了 20=1、21=2、22=4、23=8 的输入,没有任何输入时显示为 0
,如果 引脚 1
(对应 20=1)像上图一样有输入,则显示 0 + 1 = 1
。
我们来理解一下上方的电路:
如果仅打开一个输入,但不同时打开两个输入,则此处的 XOR 门(异或门)将打开,此时对应输入 引脚 1
,显示 数字 1
(类似于 1 + 0 和 0 + 1
);
如果两个输入均打开,则 AND 门(与门)将打开,此时对应输入 引脚 2
,显示 数字 2
(类似于 1 + 1
);
如果没有输入,则 AND 门和 XOR 门都保持关闭,此时显示 数字 0
(类似于 0 + 0
);
因此,如果两个都打开,则 XOR 保持关闭,并且 AND 门打开,得出正确的答案为 2
:
但这只是最基础的半加法运算器,不是太有用,因为它只能解决最简单的数学问题之一。但如果我们把它们两个与另一个输入连接,就会得到一个完整的加法器:
仔细思考几遍,你就会得知这个三个输入的加法器已经可以计算 3
个二进制数字的加法运算了,我们如法炮制,可以通过连接更多的"进位"来使这个加法器能够运算更多的数,这当然也意味着这个计算链条更长。
大多数其他数学运算都可以加法完成。乘法只是重复加法,减法可以通过一些奇特的位反转来完成,而除法只是重复减法。并且,尽管所有现代计算机都具有基于硬件的解决方案以加快更复杂的操作,但从技术上讲,您可以使用完整的加法器来完成全部操作。
现在,我们的计算机只不过是一个计算器,它记不住任何内容也对输出没有任何操作,上述电路只是接了一个显示单元而已。
上面展示的是一个存储单元。它使用了大量的 NAND 门,并且在实际生产中,根据存储技术的不同,它们可能会大不相同,但其功能是相同的。
您给它一些输入,并打开“写”位(Write
输入 1
),它将把输入存储在单元内。这不仅是一个存储单元,因为我们还需要一种从中读取信息的方法。这是通过一个使能器完成的,该使能器是「存储器」中每个位的“与”门的集合,所有位都与另一个输入(即“读取”位)绑定在一起。写入和读取位通常也称为“设置”(set
)和“启用”(enable
)。
上面整个存储单元都包裹在所谓的寄存器中。这些寄存器连接到 总线,总线是围绕整个系统运行的一束电线,并连接到每个组件。即使现代计算机也具有总线,尽管它们可能具有多个总线以提高多任务处理性能。
每个寄存器仍有一个读写位,但是在这种设置下,输入和输出是一样的。这实际上很好。例如:如果要将 R1 的内容复制到 R2,则应打开 R1 的读取位,这会将 R1 的内容压入总线。当读取位打开时,您将打开 R2 的写入位,这会将总线内容复制到 R2 中。
寄存器也用于制作 RAM。RAM 通常布置在网格中,并且导线有两个方向:
解码器采用二进制输入并打开相应的编号线。例如,11
在二进制数中是 3
,即最高的 2
位数字,因此解码器将打开最高的线路。每个路口都有一个寄存器。所有这些都连接到中央总线以及中央写入和读取输入。只有跨寄存器的两条导线也都打开时,读和写输入才会打开,从而有效地允许您选择要从中进行读写的寄存器。同样,现代 RAM 要复杂得多,但是此设置仍然有效。
寄存器无处不在,是在 CPU 中移动数据并将信息存储在 CPU 中的基本工具。那么,是什么告诉他们移动数据的呢?
时钟是 CPU 核心中的第一个组件,它将按设置的时间间隔(以赫兹或每秒周期为单位)关闭和打开。这就是您看到的最直观的 CPU 速度指标。
时钟具有三种不同的状态:基本时钟,使能时钟和设置时钟。基本时钟将打开半个周期,另一半关闭。使能时钟用于打开寄存器,并且需要更长的时间才能确保数据被使能。设置时钟必须始终与使能时钟同时打开,否则可能会写入错误的数据。
时钟连接到步进器,步进器将从 1
到最大步数进行计数,并在完成后将自身重置为 1
。时钟还连接到 CPU 可以写入的每个寄存器的 AND 门:
这些 “与” 门还连接到另一个组件的输出,即指令解码器。指令解码器接受 SET R2 TO R1
之类的指令,并将其解码为 CPU 可以理解的内容。它有自己的内部寄存器,称为“指令寄存器”,该寄存器存储了当前操作。它的精确程度取决于您正在运行的系统,但是一旦解码,它将打开正确的设置并启用正确寄存器的位,这些寄存器将根据时钟触发。
程序指令存储在 RAM(或现代系统中的 L1 高速缓存,更靠近 CPU)中。由于程序数据与其他所有变量一样都存储在寄存器中,因此可以随时对其进行操作以在程序中跳转。这就是程序通过循环和 if
语句获取结构的方式。跳转指令将指令解码器正在读取的存储器中的当前位置设置到其他位置。
现在,我们对 CPU 工作原理的有了一些基本的了解。主总线跨越整个系统,并连接到所有寄存器。完整的加法器以及其他一系列运算都打包在算术逻辑单元或 ALU 中。该 ALU 将与总线建立连接,并且还将具有自己的寄存器来存储正在操作的第二个数字。
为了执行计算,将程序数据从系统 RAM 加载到控制部分。控制部分从 RAM 中读取两个数字,将第一个数字加载到 ALU 的指令寄存器中,然后将第二个数字加载到总线上。同时,它向 ALU 发送指令代码,告知其操作方法。然后,ALU 执行所有计算,并将结果存储在另一个寄存器中,CPU 可以从该寄存器中读取该值,然后继续该过程。
原来,我们是这样记数的 - https://www.jianshu.com/p/58844323e4fb
二进制数的运算方法 - https://www.jianshu.com/p/560aba49c9a4
文字,图片,视频,音频的二进制表示 - https://blog.csdn.net/c46550/article/details/91040925
知乎 - 计算机只认识0和1但是怎么表示图像和影视等等众多应用的?| @kross - https://www.zhihu.com/question/36269548
Introduction to binary numbers - https://pmihaylov.com/intro-binary-numbers/
What is Binary, and Why Do Computers Use It? - https://www.howtogeek.com/367621/what-is-binary-and-why-do-computers-use-it/
CPU 是怎样认识代码的?| 知乎 - https://www.zhihu.com/question/348237008/answer/843382847 | @Zign
HTG Explains: How Does a CPU Actually Work? - https://www.howtogeek.com/367931/htg-explains-how-does-a-cpu-actually-work/
特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:
长按订阅更多精彩▼
如有收获,点个在看,诚挚感谢