计算机的基本硬件组成
早年,要自己组装一台计算机,要先有三大件,CPU、内存和主板。
在这三大件中,我们首先要说的是CPU,它是计算机最重要的核心配件,全名叫中央处理器(Central Processing Unit)。为什么说CPU是"最重要"的呢?因为计算机的所有"计算"都是由CPU来进行的。自然,CPU也是整台计算机中造价最昂贵的部份之一。
第二个重要的配件,就是内存(Memory)。你撰写的程序、打开的浏览器、运行的游戏,都要加载到内存里才能运行。程序读取的数据、计算得到的结果,也都要放在内存里。内存越大, 能加在的东西自然也就越多。
存放在内存里的程序和数据,需要被CPU读取,CPU计算完之后,还要把数据写回到内存。然而CPU不能直接插到内存上,反之亦然。于是,就带来了最后一个大件----主板(Motherboard)。
主板是一个有着各种各样,有时候多达数十乃至上百个插槽的配件。我们的CPU要插在主板上,内存也要插在主板上。主板的芯片组(Chipset)和总线(Bus)解决了CPU和内存之间如何通信的问题。芯片组控制了数据传输的流转,也就是数据从哪里到哪里的问题。总线则是实际数据传输的高速公路。因此。总线速度(Bus Speed)决定了数据能传输多快。
有了三大件,只要配上电源供电,计算机差不多就可以跑起来了。但是现在还缺少各类输入(Input)/输出(Output)设备,也就是我们常说的I/O设备。如果你用的是自己的个人电脑,那显示器肯定必不可少,只有有了显示器我们才能看到计算机输出的各种图像、文字,这也就是所谓的输出设备。
同样的,鼠标和键盘也都是必不可少的配件。
最后,你自己配的个人计算机,还要配上一个硬盘。这样各种数据才能持久地保存下来。绝大部分人都会给自己的机器装上一个机箱,配上风扇,解决灰尘和散热的问题。不过机箱和风扇,算不上计算机的必备硬件。
还有一个很特殊的设备,就是显卡(Graphics Card)。现在,使用图形界面操作系统的计算机,无论是Windows、Mac OS还是Linux,显卡都是必不可少的。有人可能要说了,我装机的时候没有买显卡,计算机一样可以正常跑起来啊!那是因为,现在的主板都带了内置的显卡。如果你用计算机玩游戏,做图形渲染或者跑深度学习应用,你多半就需要买一张单独的显卡,插在主板上。显卡之所以特殊,是因为显卡里有除了CPU之外的另一个"处理器",也就是GPU(Graphics Processing Unit,图形处理器),GPU一样可以做各种"计算"的工作。
鼠标、键盘以及硬盘,这些都插在主板上的。作为外部I/O设备,它们通过主板上的南桥(SouthBridge)芯片组,来控制和CPU之间的通信。"南桥"芯片的名字很直观,一方面,它在主板上的位置,通常在主板的"南面"。另一方面,它的作用就是作为"桥",来连接鼠标、键盘以及硬盘这些外部设备和CPU之间的通信。
有了南桥,自然对应着也有"北桥"。是的,以前的主板上通常也有"北桥"芯片,用来作为"桥",连接CPU和内存、显卡之间的通信。不过,随着时间的变迁,现在的主板上的"北桥"芯片的工作,已经被迁移到了CPU的内部,所以你在主板上,已经看不到北桥芯片了。
冯·诺伊曼体系结构
刚才我们讲了一台计算机的硬件组成,这说的是我们平时用的个人电脑或者服务器。那我们平时最常用的智能手机的组成,也是这样吗?
我们手机里只有SD卡(Secure Digiral Memory Card)这样类似硬盘功能的存储卡插槽,并没有内存插槽、CPU插槽这些东西。没错,因为手机尺寸的原因,手机制造商们选择把CPU、内存、网络通信,乃至摄像头芯片,都封装到一个芯片,然后再嵌入到手机主板上。这种方式叫SOC,也叫System on a Chip(系统芯片)。
这样看起来,个人电脑和智能手机的硬件组成方式不太一样。可是,我们写智能手机上的App,和写个人电脑的客户端应用似乎没有什么差别,都是通过"高级语言"这样的编程语言撰写、编译之后,一样是把代码和数据加载到内存里来执行。这是为什么呢?因为,无论是个人电脑、服务器、智能手机,还是Raspberry Pi这样的微型卡片机,都遵循着同一个"计算机"的抽象概念。这是怎么样一个"计算机"呢?这其实就是,计算机祖师爷之一冯·诺伊曼体系结构(Von Neumann architecture),也叫存储程序计算机。
什么是存储程序计算机呢?这里面其实暗含了两个概念,一个是"可编程"计算机,一个是"存储"计算机。
说到"可编程",估计你会有点懵,你可以先想想,什么是"不可编程"。计算机是由各种门电路组合而成的,然后通过组装出一个固定的电路板,来完成一个特定的计算程序。一旦需要修改功能,就要重新组装电路。这样的话,计算机就是"不可编程"的,因为程序在计算机硬件层面是"写死"的。最常见的就是老式计算器,电路板设计好了加减乘除,做不了任何计算逻辑固定之外的事情。
我们再来看"存储"计算机。这其实是说,程序本身是存储在计算机内存里,可以通过加载不同的程序来解决不同的问题。有"存储程序计算机",自然也有不能存储程序的计算机。典型的就是早年的"Plugboard"这样的插线板式的计算机。整个计算机就是一个巨大的插线板,通过在板子上不同的插头后者接口的位置插入线路,来实现不同的功能。这样的计算机自然是"可编程"的,但是编写好的程序不能存储下来供下一个加载使用,不得不每次要用到和当前不同的"程序"的时候,重新插板子,重新"编程"。
可以看到,无论是"不可编程"还是"不可存储",都会让使用计算机的效率大大下降。而这个对于效率的追求,也就是"存储程序计算机"的由来。
于是我们的祖师爷,基于当是在秘密开发的EDVAC写了一篇报告First Draft of a Report on the EDVAC,描述了他心目中的一台计算机应该长什么样。这篇报告在历史上有个很特殊的简称,叫First Draft,翻译成中文,其实就是《第一份草案》。这样,现代计算机的发展就从祖师爷写的一份草案开始了。
First Fraft里面说了一台计算机应该有哪些组成,我们一起来看看。
首先是一个包含算术逻辑单元(Arithmetic Logic Unit,ALU)和处理器寄存器(Processor Register)的处理器单元(Processing Unit),用来完成各种算术和逻辑运算。因为它能够完成各种数据的处理或者计算工作,因此也有人把这个叫做数据通路(Datapath)或者运算器。
然后是一个包含指令寄存器(Instruction Register)和 程序计数器(Program Counter)的控制器单元(Control Unit, CU),用来控制程序的流程,通常就是不同条件下的分支和跳转。在现在的计算机里,上面的算术逻辑单元和这里的控制器单元,共同组成了我们说的CPU。
接着是用来存储数据(Data)和指令(Instruction)的内存。以及更大容量的外部存储,在过去,可能是磁带、磁鼓这样的设备,现在通常就是硬盘。
最后就是各种输入和输出设备,以及对应的输入输出机制。我们现在无论是使用什么样的计算机,其实都是和输入输出设备在打交道。个人电脑的鼠标键盘是输入设备,显示器是输出设备。我们用的智能手机,触摸屏即是输入设备,又是输出设备。而跑在各种云上的服务器,则是通过网络来进行输入和输出。这个时候,网卡即是输入设备又是输出设备。
任何一台计算机的任何一个部件都可以归到运算器、控制器、存储器、输入设备和输出设备中,而所有现代计算机也是基于这个基础架构来设计开发的。
而所有的计算机程序,也都可以抽象为从输入设备读取输入信息,通过运算器和控制器来执行存储在存储器里的程序,最终把结果输出到输出设备中。而我们所有撰写的无论高级还是低级语言,也都是基于这样一个抽象框架来进行运行的。
总结延伸
可以说,冯·诺伊曼体系结构确立了我们现在使用的计算机的基础架构。因此,学习计算机组成原理,其实就是学习和拆解冯·诺伊曼体系结构。
具体来说,学习组成原理,其实就是学习控制器、运算器的工作原理,也就是CPU是怎么工作的,以及为何这样设计;学习内存的工作原理,从最基本的电路,到上层抽象给到CPU乃至应用程序的结构是怎样的;学习CPU是怎么和输入设备、输出设备打交道的。
学习组成原理,就是在理解从控制器、运算器、存储器、输入设备和输出设备,从电路这样的硬件,到最终开放给软件的接口,是怎么运作的,为什么要设计成这样,以及在软件开发层面尽可能用好它。
在这图中,整个计算机组成原理拆分为四大部分,分别是计算机的基本组成、计算机的指令和计算、处理器的设计以及存储器和I/O设备。
“性能”这个词,不管是在日常生活还是写程序的时候,都经常被提到。比方说,买新电脑的时候,我们会说“原来的电脑性能跟不上了”;写程序的时候,我们会说,“这个程序性能需要优化一下“。那么,”性能“到底指的是什么?
什么是性能?时间的倒数
计算机的性能,其实和我们干体力劳动很像,好比是我们要搬东西。对于计算机的性能,我们需要有个衡量标准,这个标准主要有两个指标。
第一个是响应时间(Response time)或者叫执行时间(Execution time)。想要提升响应时间这个性能指标,你可以理解为让计算机”跑得更快“。
第二个是吞吐率(Throughput)或者带宽(Bandwidth),想要提升这个指标,你可以理解为让计算机”搬得更多“。
所以说,响应时间指的就是,我们执行一个程序,到底需要花多少时间。花的时间越少,自然性能就越好。
而吞吐率就是指我们在一定的时间范围内,到底能处理多少事情。这里的”事情“,在计算机里就是处理的数据或者执行的程序指令。
和搬东西来做对比,如果我们的响应时间短,跑得快,我们可以来回多跑几趟多搬几趟。所以说,缩短程序的响应时间,一般来说都会提升吞吐率。
除了缩短响应时间,我们还有别的方法吗?当然有,比如说,我们还可以找几个人一起来搬,这就类似现代的服务器都是8核、16核的。人多力量大,同时处理数据,在单位时间内就可以处理更多数据,吞吐率自然也就上去了。
提升吞吐率的办法有很多。大部分时候,我们只要多加一些机器,多堆一些硬件就好了。但是响应时间的提升却没有那么容易,因为CPU的性能提升其实在10年前就处于”挤牙膏“的状态了,所以我们得慎重地来分析对待。下面我们具体来看:
我们一般把性能,定义为响应时间的倒数,也就是
性能 = 1 / 响应时间
这样一来,响应时间越短,性能的数值就越大。同样一个程序,在Inter最新的CPU Coffee Lake上,只需要30s就能运行完成,而在5年前的CPU Sandy Bridge上,需要1min才能完成,那么我们自然可以算出来,Coffee Lake的性能1 / 30,Sandy Bridge的性能是1 / 60,两个的性能比为2,于是,我们就说,Coffee Lake的性能是Sandy Bridge的2倍。
过去几年流行的手机跑分软件,就是把多个预设好的程序在手机上运行,然后根据运行需要的时间,算出一个分数来给手机的性能评估。而在业界,各大CPU和服务器厂商组织了一个叫做SPEC的第三方机构,专门用来指定各种”跑分“的规则。
计算机的计时单位:CPU时钟
虽然时间是一个很自然的用来衡量性能的指标,但是用时间来衡量,有两个问题。
第一个是时间不”准“。如果用你自己随便写的一个程序,来统计程序运行时间,每一次统计结果不会完全一样。有可能这一次花了45ms,下一次变成了53ms。
为什么会不准呢?这里面有好几个原因。首先,我们统计时间是用类似于”掐秒表“一样,记录程序运行结束的时间减去程序开始运行的时间。这个时间也叫Wall Clock Time或者Elapsed Time,就是在运行程序期间,挂在墙上的钟走掉的时间。
但是,计算机可能同时运行着好多程序,CPU实际上不停地在各个程序之间进行切换。在这些走掉的时间里面,很可能CPU切换去运行别的程序了。而且,有些程序运行的时候,可能要从网络、硬盘去读取数据,要等网络和硬盘把数据读出来,给到内存和CPU。所以说,要想准确统计某个程序运行时间,进而去比较两个程序的实际性能,我们得把这些时间给刨除掉。
那这件事怎么实现呢?Linux下有个叫time的命令,可以帮我们统计出来,同样的Wall Clock Time下,程序实际在CPU上到底花了多少时间。
我们简单运行一下time命令。它会返回三个值,第一个是real time,也就是我们说的Wall Clock Time,也就是运行程序整个过程中流逝掉的时间;第二个是user time,也就是CPU在运行你的程序,在用户态运行指令的时间;第三个是sys time,是CPU在运行你的程序,在操作系统内核里运行指令的时间。而程序实际花费的CPU执行时间(CPU Time),就是user time + sys time
```
timeseq1000000|wc-l
1000000
real 0m0.101s
user 0m0.031s
sys 0m0.016s
```
在这个例子里,可以看到,实际上程序用了0.101s,但是CPU time只有0.031s + 0.014s = 0.047s。运行程序的时间里,只有不到一半是实际花在这个程序上的。
其次,即使我们已经拿到了CPU时间,我们也不一定可以直接”比较“出两个程序的性能差异。即使在同一台计算机上,CPU可能满载运行也可能降频运行,降频运行的时候自然话的时间会多一些。
除了CPU之外,时间这个性能指标还会收到主板、内存这些其他相关硬件的影响。所以,我们需要对”时间”这个我们可以感知的指标进行拆解,把程序的CPU执行时间变成CPU时钟周期数(CPU Cyles)和时钟周期时间(Clock Cycle)的乘积。
程序的CPU执行时间 = CPU时钟周期数 * 时钟周期时间
我们先来理解一下什么是时钟周期时间。你在买电脑的时候,一定关注过CPU的主频。比如我手头的这台电脑就是Inter Core-i7-7700HQ 2.8GHz,这里的2.8GHz就是电脑的主频(Frequency/Clock Rate)。这个2.8GHz,我们可以先粗浅地认为,CPU在1s时间内,可以执行的简单指令的数量是2.8G条。
如果想要更准确一点描述,这个2.8GHz就代表,我们CPU的一个“钟表“能够识别出来的最小时间间隔。就像我们挂在墙上的挂钟,都是”滴答滴答“一秒一秒地走,所以通过墙上的挂钟能够识别出来的最小时间单位就是秒。
而在CPU内部,和我们平时戴的石英表类似,有一个叫晶体振荡器(Oscillator Crystal)的东西,简称晶振。我们把晶振当成CPU内部的电子表来使用。晶振带来的每一次”滴答“,就是时钟周期时间。
在这个2.8GHz的CPU上,这个时钟周期时间,就是1 / 2.8G。我们的CPU,是按照这个”时钟“提示的时间来进行自己的操作。主频越高,意味着这个表走得越快,我们的CPU也就”被逼“着走的越快。
如果你自己组装过台式机的话,可能听说过”超频“这个概念,着说的其实就相当于把买回来的CPU内部的钟给调快了,于是CPU的计算跟着这个时钟的节奏,也就自然变快了。当然这个快不是没有代价的,CPU跑得越快,散热的压力也就越大。就和人一样,超过生理极限,CPU就会崩溃。
我们现在回到上面程序CPU执行时间的公式。
程序的CPU执行时间 = CPU时钟周期数 * 时钟周期时间
最简单的提升性能方案,自然缩短时钟周期时间,也就是提升主频。换句话说,就是换一块好一点的CPU。不过,这个是我们软件工程师控制不了的事情,所以我们把目光挪到了乘法的另一个因子---CPU时钟周期数上。如果能够减少程序需要的CPU时钟周期数,一样能够提升程序性能。
对于CPU时钟周期数,我们可以再做一个分解,把它变成”指令数 * 每条指令的平均时钟周期数(Cycles Per Instruction,简称CPI)“。不同的指令需要的Cycles是不同的,加法和乘法都对应一条CPU指令,但是乘法需要的Cycles就比加法要多,自然也就慢。在这样拆分之后,我们的程序CPU执行时间就可以变成这样三个部分的乘积。
程序的CPU执行时间 = 指令数 * CPI * Clock Cycle Time
因此,如果我们想要解决性能问题,其实就是要优化这三者。
1.时钟周期时间,就是计算机的主频,这个取决于计算机硬件。我们所熟知的摩尔定律就一直在不停地提高我们计算机的主频。比如说,最早的80386主频只有33MHz,现在手头的笔记本电脑就是2.8GHz,在主频层面,就提升了将近100倍。
2.每条指令的平均时钟周期数CPI,就是一条指令到底需要多少CPU Cycle。在后面讲解CPU结构的时候,我们会看到,现代的CPU通过流水线技术(Pipeline),让一条指令需要的CPU Cycle尽可能的少。因此,对于CPI的优化,也是计算机组成和体系结构中重要一环。
3.指令数,代表执行我们程序到底需要多少条指令、用哪些指令。这个很多时候就把挑战给了编译器。同样的代码,编译成计算机指令的时候,就有各种不同的表示方式。
我们可以把自己想象成一个CPU,坐在那里写程序。计算机主频就好像是你的打字速度,打字越快,你自然可以多写一点程序。CPI相当于你在写程序的时候,熟悉各种快捷键,越是打相同的内容,需要敲击键盘的次数就越少。指令数相当于你的程序设计得够合理,同样的程序要写的代码行数就少。如果三者皆能实现,你自然可以很快地写出一个优秀的程序,你的”性能“从外面来看就是好的。
在上一讲,提到了这样一个公式:程序的CPU执行时间 = 指令数 * CPI * Clock Cycle Time这么看来,如果要提升计算机的性能,我们可以从指令数、CPI以及CPU主频这三个地方入手。要搞定指令数或者CPI,乍一看都不太容易。于是,研发CPU的硬件工程师们,从80年代开始,就挑上了CPU这个“软柿子”。在CPU上多放一点晶体管,不断提升CPU的时钟频率,这样就能让CPU变得更快,程序的执行时间就会缩短。于是,从1978年Intel发布的8086CPU开始,计算机的主频从5MHz开始,不断提升。1980年代中期的80386能够跑到40MHz,1989年的486能够跑到100MHz,直到2000年的奔腾4处理器,主频已经到达了1.4GHz。而消费者也在这20年里养成了“看主频”买电脑的习惯。当时已经基本垄断了桌面CPU市场的Intel更是夸下了海口,表示奔腾4所使用的CPU结构可以做到10GHz,颇有“大力出奇迹”的意思。功耗:CPU的“人体极限”然而,计算机科学界从来不相信“大力出奇迹”。奔腾4的CPU主频从来没有达到过10GHz,最终它的主频上限定格在3.8GHz。这还不是最糟的,更糟糕的事情是,大家发现,奔腾4的主频虽然高,但是它的实际性能却配不上同样的主频。想要用在笔记本上的奔腾4 2.4GHz处理器,其性能之和基于奔腾3架构的奔腾M 1.6GHz处理器差不多。于是,这一次的“大力出悲剧”,不仅让Intel的对手AMD获得了喘息之机,更是代表着“主频时代”的终结。后面几代Intel CPU主频不但没有上升,反而下降了。到如今,2019年的最高配置Intel i9 CPU,主频也之不过是5GHz而已。相较于1978年到2000年,这20年里300倍的主频提升,从2000年到现在的这19年,CPU的主频大概提高了3倍。
奔腾4的主频为什么没能超过3.8GHz的障碍呢?答案就是功耗问题。什么功耗问题呢?我们先看一个直观的例子。
一个3.8GHz的奔腾4处理器,满载功率是130瓦。这个130瓦是什么概念呢?机场允许带上飞机的充电宝的容量上限是100瓦。如果我们把这个CPU安在手机里面,不考虑屏幕内存之类的耗电,这个CPU满载运行45分钟,充电宝里面就没电了。而iphone x使用ARM架构的CPU,功率则只有4.5瓦左右。
我们的CPU,一般都被叫做超大规模集成电路(Very-Large-Scale Intergration,VLSI)。这些电路,实际上都是一个个晶体管组合而成的。CPU在计算,其实就是让晶体管里面的“开关”不断地去“打开”和“关闭”,来组合完成各种运算和功能。
想要计算得快,一方面,我们要在CPU里,同样的面积里面,多放一些晶体管,也就是增加密度;另一方面,我们要让晶体管“打开”和“关闭”得更快一点,也就是提升主频。而这两者,都会增加功耗,带来耗电和散热的问题。
这么说,可能还是有点抽象,我还是举一个例子。你可以把一个计算机CPU想象成一个巨大的工厂,里面有很多工人,相当于CPU上面的晶体管,互相之间协同工作。
为了工作得快一点,我们要在工厂里多塞一点人。你可能会问,为什么不把工厂造得大一点呢?这时因为,人和人之间如果离得远了,互相之间走过去需要花费的时间就变长,这也会导致性能的下降。这就好像如果CPU的面积大,晶体管之间的距离变大,电信号传输的时间就会边长,运算速度自然就慢了。
除了多塞一点人,我们还希望每个人的动作都快一点,这样同样的时间里就可以多干一点活了。这就相当于提升CPU的主频,但是动作快,每个人就要出汗散热。要是太热了,对工厂里面的人来说就会中暑生病,对CPU来说就会崩溃出错。
我们会在CPU上面抹硅脂、装电扇,乃至用上水冷或者其他更好的散热设备,就好像在工厂里面装风扇、空调,发冷饮一样。但是同样的空间下,装上风扇空调能够带来的散热效果也是有极限的。
因此,在CPU里面,能够放下的晶体管数量和晶体管的“开关”频率也都是有限的。一个CPU的功率,可以用这样一个公式来表示:
功耗 ~= 1 / 2 * 负载电容 * 电压的平方 * 开关频率 * 晶体管数量
那么,为了要提高性能,我们需要不断地增加晶体管的数量。同样的面积下,我们想要多放一点晶体管,就要把晶体管造得小一点。这个就是平时我们所说的提升“制程”。从28nm到7nm,相当于晶体管本身变成了原来的1 / 4大小。这个就相当于我们在工厂里,同样的活,我们要找瘦小一点的工人,这样一个工厂里面就可以多一些人。我们还要提升主频,让开关的频率变快,也就是要找手脚更快的工人。
但是,功耗增加太多,就会导致CPU散热跟不上,这时,我们就需要降低电压。这里有一点非常关键,在整个功耗的公式里面,功耗和电压的平方成正比。这意味着电压下降到原来的1 / 5,整个的功耗会变成原来的1 / 25。
事实上,从5MHz主频的8086到5GHz的Intel i9,CPU的电压已经从5V左右下降到1V左右。这也是为什么我们CPU的主频提升了1000倍,但是功耗只增加了40倍。
并行优化,理解阿姆达尔定律
虽然制程的优化和电压的下降,在过去的 20 年里,让我们的 CPU 性能有所提升。但是从上世纪九十年代到本世纪初,软件工程师们所用的“面向摩尔定律编程”的套路越来越用不下去了。“写程序不考虑性能,等明年 CPU 性能提升一倍,到时候性能自然就不成问题了”,这种想法已经不可行了。于是,从奔腾 4 开始,Intel 意识到通过提升主频比较“难”去实现性能提升,边开始推出 Core Duo 这样的多核 CPU,通过提升“吞吐率”而不是“响应时间”,来达到目的。提升响应时间,就好比提升你用的交通工具的速度,比如原本你是开汽车,现在变成了火车乃至飞机。本来开车从上海到北京要 20 个小时,换成飞机就只要 2 个小时了,但是,在此之上,再想要提升速度就不太容易了。我们的 CPU 在奔腾 4 的年代,就好比已经到了飞机这个速度极限。
那你可能要问了,接下来该怎么办呢?相比于给飞机提速,工程师们又想到了新的办法,可以一次同时开 2 架、4 架乃至 8 架飞机,这就好像我们现在用的 2 核、4 核,乃至 8 核的 CPU。虽然从上海到北京的时间没有变,但是一次飞 8 架飞机能够运的东西自然就变多了,也就是所谓的“吞吐率”变大了。所以,不管你有没有需要,现在 CPU 的性能就是提升了 2 倍乃至 8 倍、16 倍。这也是一个最常见的提升性能的方式,通过并行提高性能。这个思想在很多地方都可以使用。举个例子,我们做机器学习程序的时候,需要计算向量的点积,比如向量 W=[W0,W1,W2,…,W15] 和向量 X=[X0,X1,X2,…,X15],W⋅X=W0∗X0+W1∗X1+ W2∗X2+…+W15∗X15。这些式子由 16 个乘法和 1 个连加组成。如果你自己一个人用笔来算的话,需要一步一步算 16 次乘法和 15 次加法。如果这个时候我们把这个任务分配给 4 个人,同时去算 W0~W3, W4~W7, W8~W11, W12~W15 这样四个部分的结果,再由一个人进行汇总,需要的时间就会缩短。
但是,并不是所有问题,都可以通过并行提高性能来解决。如果想要使用这种思想,需要满足这样几个条件。第一,需要进行的计算,本身可以分解成几个可以并行的任务。好比上面的乘法和加法计算,几个人可以同时进行,不会影响最后的结果。第二,需要能够分解好问题,并确保几个人的结果能够汇总到一起。第三,在“汇总”这个阶段,是没有办法并行进行的,还是得顺序执行,一步一步来。这就引出了我们在进行性能优化中,常常用到的一个经验定律,阿姆达尔定律(Amdahl’s Law)。这个定律说的就是,对于一个程序进行优化之后,处理器并行运算之后效率提升的情况。具体可以用这样一个公式来表示:优化后的执行时间 = 受优化影响的执行时间 / 加速倍数 + 不受影响的执行时间
在刚刚的向量点积例子里,4 个人同时计算向量的一小段点积,就是通过并行提高了这部分的计算性能。但是,这 4 个人的计算结果,最终还是要在一个人那里进行汇总相加。这部分汇总相加的时间,是不能通过并行来优化的,也就是上面的公式里面不受影响的执行时间这一部分。比如上面的各个向量的一小段的点积,需要 100ns,加法需要 20ns,总共需要 120ns。这里通过并行 4 个 CPU 有了 4 倍的加速度。那么最终优化后,就有了 100/4+20=45ns。即使我们增加更多的并行度来提供加速倍数,比如有 100 个 CPU,整个时间也需要 100/100+20=21ns。
总结延伸我们可以看到,无论是简单地通过提升主频,还是增加更多的 CPU 核心数量,通过并行来提升性能,都会遇到相应的瓶颈。仅仅简单地通过“堆硬件”的方式,在今天已经不能很好地满足我们对于程序性能的期望了。于是,工程师们需要从其他方面开始下功夫了。在“摩尔定律”和“并行计算”之外,在整个计算机组成层面,还有这样几个原则性的性能提升方法。1.加速大概率事件。最典型的就是,过去几年流行的深度学习,整个计算过程中,99% 都是向量和矩阵计算,于是,工程师们通过用 GPU 替代 CPU,大幅度提升了深度学习的模型训练过程。本来一个 CPU 需要跑几小时甚至几天的程序,GPU 只需要几分钟就好了。Google 更是不满足于 GPU 的性能,进一步地推出了 TPU。后面的文章,我也会为你讲解 GPU 和 TPU 的基本构造和原理。
2.通过流水线提高性能。现代的工厂里的生产线叫“流水线”。我们可以把装配 iPhone 这样的任务拆分成一个个细分的任务,让每个人都只需要处理一道工序,最大化整个工厂的生产效率。类似的,我们的 CPU 其实就是一个“运算工厂”。我们把 CPU 指令执行的过程进行拆分,细化运行,也是现代 CPU 在主频没有办法提升那么多的情况下,性能仍然可以得到提升的重要原因之一。我们在后面也会讲到,现代 CPU 里是如何通过流水线来提升性能的,以及反面的,过长的流水线会带来什么新的功耗和效率上的负面影响。3.通过预测提高性能。通过预先猜测下一步该干什么,而不是等上一步运行的结果,提前进行运算,也是让程序跑得更快一点的办法。典型的例子就是在一个循环访问数组的时候,凭经验,你也会猜到下一步我们会访问数组的下一项。后面要讲的“分支和冒险”、“局部性原理”这些 CPU 和存储系统设计方法,其实都是在利用我们对于未来的“预测”,提前进行相应的操作,来提升我们的程序性能。好了,到这里,我们讲完了计算机组成原理这门课的“前情提要”。一方面,整个组成乃至体系结构,都是基于冯·诺依曼架构组成的软硬件一体的解决方案。另一方面,你需要明白的就是,这里面的方方面面的设计和考虑,除了体系结构层面的抽象和通用性之外,核心需要考虑的是“性能”问题。