ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

计算机组成原理:硬件设计思想与软件落地的核心脉络

计算机组成原理:硬件设计思想与软件落地的核心脉络 学计算机组成原理的人十个里有八个是把教材当字典在啃。今天背一个ALU明天背一个Cache后天背一个DMA概念认识一大堆可真要问他“CPU到底怎么把一条加法指令从头到尾执行完”多半只能挤出“取指、译码、执行”六个字再往下就卡壳了。我当年也走过这个弯路后来反复折腾才想明白问题不在记忆力是视角错了。平时我们写代码、装系统、玩游戏用的是“使用者视角”CPU够快、内存够大就行但组成原理这门课逼着你站到“设计者视角”重新看机器。这篇是“计算机组成原理”系列的第二篇主题是“计算机硬件设计思想及软件”我结合自己啃教材、做实验、再到后来帮别人讲题的折腾经历把设计者视角下最核心的几条思想脉络捋一遍再看软件如何顺着这套硬件思想一步步落地。如果你正在被组成原理折磨、准备期末或考研又或者单纯想知道程序在机器里到底怎么跑起来这篇应该能帮上点忙。1. 先换视角使用者不关心的事设计者全得操心1.1 两个世界的复杂度不一样普通程序员看计算机关心的是“我这台电脑CPU是几核、主频多高、内存多大”这些本质上是参数。设计者看计算机关心的是完全不同的东西指令从内存取出来之后怎么解码寄存器堆需要几个读端口、几个写端口访存阶段和数据写回阶段会不会撞车分支预测错了怎么把流水线洗干净差别有多大你在C语言里写一句sum a[i]使用者只关心结果对不对、跑得快不快。但到了组成原理里这句话先要翻译成若干条机器指令每条指令在数据通路上要经历取指、译码、执行、访存、写回好几个阶段加载数据的访存动作可能命中L1 Cache也可能没命中没命中就得停顿等待。同一个动作两个世界看到的复杂度完全不同。1.2 硬件的四个约束条件硬件设计不是“拍脑袋定方案”它是在约束条件之间找平衡。计算机体系结构里有个最基础的性能公式几乎所有设计决策都跟它有关CPU时间 指令数 × 每指令周期数(CPI) × 时钟周期时间这个公式最坑的地方在于三个变量是互相牵制的。你提高了时钟频率可能因为流水线停顿增加CPI你为了减少指令数而把指令设计得很复杂结果每条指令的执行周期又变长。所以设计者不是在“把某一个指标做到最大”而是在一堆指标里找平衡点。约束条件主要影响典型例子性能吞吐率、延迟、实时性服务器CPU追求高吞吐嵌入式追求低延迟成本芯片面积、良率、售价低端单片机面积小、良率高价格几块钱功耗续航、散热、电费手机芯片必须控制功耗否则电池扛不住可扩展性能否加核、加Cache、加外设x86长期兼容旧接口就是为了可扩展性这四个约束往往不能兼得。手机芯片不能用服务器级别的功耗做性能服务器芯片也不能用手机级别的成本去造。设计者做的所有事情本质上都是在“行不行”和“划不划算”之间做选择。一个功能如果用硬件实现能节省很多指令但硬件成本翻倍那不如交给软件反过来如果软件实现的开销太大、太不稳定那就得上硬件。这就是软硬件协同设计的第一课什么事情该交给硬件什么事情该留给软件。1.3 你写的代码正在替硬件做决定很多人没意识到高级语言里的一个分支、一个循环其实是在帮硬件少做不确定的事。CPU里的分支预测器会猜跳转方向猜错了就要冲刷流水线浪费十几个时钟周期。所以编译器会把大概率执行的分支放在前面这正是“设计者视角”对软件的影响。理解这一点之后你再看性能优化、再看指令集设计会突然明白很多“为什么”。2. 层次化与模块化计算机能被造出来的根本原因2.1 每一层只对上一层负责一台计算机从最底层的晶体管到最顶层的整机系统直接设计的话谁也搞不定。所以硬件设计的第一条主线就是分层晶体管组成逻辑门逻辑门组成加法器、乘法器等运算部件运算部件加上寄存器组组成数据通路和控制器再往上挂上存储器、输入输出接口最后组成整个计算机系统。这个过程跟你写代码做模块化是一模一样的。底层模块只需要把接口定义清楚上层调的时候根本不用关心内部实现用了几个晶体管、走的是什么电路。正因如此几千亿个晶体管的CPU才能由成千上万个工程师并行开发——每个人只需要管好自己那一层守住自己那部分接口。2.2 组合逻辑和时序逻辑硬件里的“函数”和“状态”数字电路里最关键的一个划分就是组合逻辑和时序逻辑。组合逻辑的输出只取决于当前输入比如加法器A和B进来立刻就出结果它没有“记忆”就像编程里的纯函数。时序逻辑则不一样输出不仅取决于当前输入还取决于之前的输入因为它内部有记忆元件——触发器。触发器可以看成是硬件里的“变量”能存1位二进制数据。一排触发器合在一起就是寄存器寄存器的集合就是寄存器堆。CPU之所以能一步一步执行指令是因为时钟信号像节拍器一样把时序逻辑的状态一步步往前推。没有组合逻辑数据算不了没有时序逻辑状态存不了。整个CPU其实就是这两种逻辑按规则交织出来的产物。2.3 总线就是模块之间的“契约”模块和模块之间要通信就得约定好规矩这个规矩在硬件里叫总线协议。一台计算机里有好几类总线地址总线用来告诉内存“我要访问哪里”数据总线用来传“内容是什么”控制总线用来发“读还是写、接下来怎么办”。你可以把总线想象成两个部门之间的工作交接标准。A部门把文件放在固定位置B部门按固定时间去取双方不用互相盯着对方干活只要都遵守那张交接单就行。硬件模块之间的接口协议也是这个道理。这也是为什么搞硬件的都很较真——接口一旦定了就不能随便改改了下游全乱。2.4 模块化带来的三个红利模块化不是“为了好看”它带来的好处非常实际。第一是可测试性你可以单独测试一个加法器、单独测试一个Cache控制器坏了好定位第二是可复用性做了一版USB控制器模块下一代芯片还能接着用第三是可分工性团队几十上百人每人认领一个模块最后拼接验证就行。这些红利同样适用于软件。你写的服务拆成微服务、你写的代码抽成公共函数本质上跟硬件设计者做模块化是同一套思路。很多开发者转到系统设计岗之后会发现硬件设计的老方法依然好用就是这个原因。3. 存储程序思想计算机能“万能”的根3.1 冯诺依曼结构到底说了什么冯诺依曼结构被提出来几十年今天所有通用计算机还在用它。它的核心思想可以概括为一句话把指令像数据一样存在存储器里CPU按顺序取出来执行。这就是“存储程序”的含义。为什么这个思想这么关键你看看计算器就明白了。计算器只能执行固化的运算逻辑功能出厂就定死了改不了而存储程序的计算机你只要往内存里放不同的指令序列也就是程序它就能变成文本编辑器、游戏机、视频播放器。硬件是同一套软件千变万化靠的就是“程序也是数据”这个伟大的抽象。冯诺依曼结构还有几个配套要点指令和数据统一编址存放在同一个存储器里计算机由运算器、控制器、存储器、输入设备、输出设备五大部件组成指令按顺序执行遇到跳转指令才改变流向。这些在今天看来平平无奇但在当年是石破天惊的。3.2 指令周期CPU从开机到关机都在重复的循环CPU干活的过程就是不断重复“取指、译码、执行、访存、写回”这几个阶段。你看着CPU主频3GHz觉得它“算得飞快”其实它只是在以每秒几十亿次的频率重复这条循环而已。具体拆开看比如执行一条lw $t0, 0($a0)从内存读数据到寄存器流程是这样的程序计数器PC先给出当前指令的地址控制器按这个地址去内存取指令取回来放进指令寄存器IR译码器分析这条指令是“访存类指令”读寄存器拿到基地址$a0的值ALU算出“基地址偏移量”得到实际内存地址然后按这个地址去内存读数据最后数据写回目标寄存器$t0同时PC自动加4一条指令的长度指向下一条指令。一句“从内存读个数”背后是五个阶段的接力。你把这个过程画成数据通路图整本组成原理书的一半就算入门了。很多学生学到最后对“数据通路”还是糊的就是因为没把这条接力路线刻在脑子里。3.3 ISA软硬件之间不能违约的合同指令集体系结构ISA是硬件设计者给软件开发者划出的一条清晰边界。它规定了这个CPU支持哪些指令、寄存器有多少个、指令是什么格式、内存怎么寻址、异常怎么上报。CPU内部的微架构怎么实现软件不关心但ISA必须严格遵守否则编译器和操作系统全得跟着改。你可以把ISA理解成一份合同硬件承诺“你按这套规则给我指令我一定给你算出结果”软件承诺“我乖乖按这套规则生成指令不越界”。分界线两边硬件工程师想的是怎么把每条指令执行得更快软件工程师想的是怎么把一段逻辑用最少指令表达出来。没有这份合同软硬件根本没法协作。4. 性能提升的底层逻辑时间并行与空间并行4.1 时间并行流水线前面说的“取指、译码、执行、访存、写回”五个阶段如果每条指令完整走完才取下一条那大部分硬件单元都是在轮流打瞌睡。于是硬件设计者想到了流水线一条指令在访存的时候下一条指令已经在执行了再下一条在译码再下一条在取指五个阶段像工厂流水线一样同时开工。流水线的理想加速比等于流水线级数5级流水线的理想加速比就是5倍。但理想只是理想流水线会遇到三类乱子结构冒险两条指令同时要用同一块硬件、数据冒险上一条指令的结果还没写回下一条就要用、控制冒险遇到了分支跳转不知道下一条该取谁。为了对付这些冒险硬件里加入了转发、停顿、分支预测等一堆机制。所以你会发现硬件设计者提高性能的手段不是把某一个部件做得飞快而是让所有部件尽量别闲着。这个思想渗透在CPU设计的每个角落。4.2 空间并行多发射、多核与SIMD时间上错开叫流水线空间上同时干就是多部件并行。超标量CPU在一个时钟周期内可以发射多条指令相当于把好多条流水线并排放在一起干活多核处理器则是直接在芯片上放多个完整的CPU核心SIMD指令则是一条指令同时处理多份数据视频编解码、图像处理全靠它提速。这三种并行的层次不一样但思路是共通的单个人干得再快也有限那就多叫几个人一起来。不过并行是有代价的多发射要做复杂的乱序执行调度多核要考虑一致性和同步SIMD要让编译器把数据排得整整齐齐。空间并行的每一个收益背后几乎都跟着一笔硬件复杂度的账单。4.3 加法器的折中从串行进位到超前进位“以空间换时间”这个思想在一个小小的加法器上就能看得很清楚。拿4位加法器来说如果把低位的进位输出接到高位的进位输入就是串行进位加法器第1位算完才能算第2位第2位算完才能算第3位最坏情况下进位信号要从最低位一路传到最高位延迟随位数线性增长32位的话延迟大得没法看。于是设计了超前进位加法器并行进位加法器通过进位生成函数和进位传递函数提前算出每一位的进位不用等前一位算完。代价是电路逻辑复杂很多位数越多门电路越夸张。实际工程里还有个折中方案叫组间串行进位小组内部用超前进位组与组之间再串行传递。这就是“组内并行、组间串行”的来历也是考试里经常出现的考点。这个例子完美诠释了硬件设计的本质并行提升性能但并行不免费它是用电路面积和功耗换来的。什么程度算“划算”永远是设计者心头最纠结的问题。4.4 存储层次用局部性制造“又快又大又便宜”主存速度跟不上CPU是硬件设计里最头疼的事之一。解决思路不是寻找一种又快又大又便宜的内存——这种物理上不存在——而是把存储器做成好几层寄存器最快但容量最小Cache次之主存再次磁盘最慢但最大。靠的是程序的局部性原理一个程序在短时间内总是反复访问一小片地址范围。你炒菜的时候调料不会放储物间而是放在灶台边随手能拿到的地方这就是Cache存在的意义。CPU设计者把最常用的数据放在L1 Cache里次常用的放在L2、L3某一层没命中再去下一层拿。正是这种层次结构让计算机在性价比上实现了“又快又大又便宜”的错觉。这也是为什么程序员如果能写出对Cache友好的代码性能差距可以拉出好几倍。5. 软件怎么落到硬件上从源码到运行的完整链路5.1 编译器把人的想法翻译成机器的词汇高级语言跟机器指令之间隔着好几层抽象。编译器负责把C、Java这类高级语言翻译成汇编语言它干的事比单纯“翻译”复杂得多要做词法分析、语法分析、语义分析还要做大量优化比如把循环里的不变量提出来、把多次访存合并。在组成原理的语境里编译器最重要的作用是决定一个程序最终会以什么样的指令组合压在硬件上。同一个函数用不同编译器、不同优化等级编译出来指令数可以差几倍。这也是设计者视角里很微妙的一点你写的代码质量最终会体现在硬件执行的指令流里。5.2 汇编器与链接器符号怎么变成地址汇编器把汇编指令翻译成机器码但此时程序里还有一堆“符号”——比如函数名main、变量名count它们还没有对应的内存地址。链接器负责把这些散装的目标文件合并成一个可执行文件把各个符号定位到最终地址再把所有引用这些符号的指令填上正确的地址。我当年学链接器的时候觉得这玩意儿跟硬件没什么关系。后来才明白链接器处理的地址分配、重定位正是指令集里“寻址方式”决定好的规则。一条call指令后面跟着的是一个偏移量这个偏移量在链接之前根本算不出来。可执行文件之所以能跑起来是因为链接器已经替它把“门牌号”全部填好了。5.3 操作系统硬件的总管家程序要运行光靠CPU和内存还不够还得有进程调度、内存管理、文件系统、设备驱动。这些全是操作系统在管。操作系统跟硬件最直接的接触点有三个中断硬件有事通知操作系统、异常指令执行出错、系统调用应用程序主动请求操作系统帮忙。拿键盘输入来说键盘控制器送上中断信号CPU暂停当前任务跳到操作系统写好的中断处理程序把按键码读进来放进缓冲区然后恢复之前的任务。你看这个过程硬件只需要提供“中断机制”这个基础设施剩下的读取、分发、处理全是软件的事。这就是软硬件分工的典型样本。5.4 ABI硬件向软件开放的“窗口尺寸”ISA规定了CPU能执行什么指令但光有指令集还不够操作系统和编译器还得约定很多细节函数参数是用寄存器传还是用栈传、哪些寄存器调用者保存、哪些被调用者保存、系统调用的编号怎么编排。这套约定就是ABI应用二进制接口。两台机器即使都是x86处理器如果ABI不同编译出来的程序也不能直接互换。ABI是ISA之上、软件之下的一层“窗口尺寸”窗口开多大、往哪开硬件设计者和软件设计者必须坐下来谈清楚。这也是为什么新设计一套指令集比如RISC-V的时候生态建设的第一步不只是设计指令还要把ABI、编译工具链、操作系统支持全铺好。6. RISC与CISC两种设计哲学在真实芯片上的交手6.1 CISC让指令干更多的活早期内存贵、容量小软件工程师希望一条指令能多干点事省得代码太长。于是x86这类CISC复杂指令集计算机处理器把很多复杂操作做成了单条指令一条指令可以同时完成“读内存、做运算、把结果存回内存”多个动作。这对编译器是友好的对代码密度是友好的但对硬件很不友好——指令长度不一致操作数类型乱七八糟译码电路复杂到飞起。为了驾驭这么复杂的指令CISC处理器普遍采用微程序控制用一段微代码来解释执行一条复杂指令。相当于硬件里藏着一个更小的解释器最底层的硬件只执行极其简单的微操作。6.2 RISC把简单留给硬件RISC精简指令集计算机是另一条路线代表人物有大卫·帕特森等人。它的核心主张很反直觉与其把指令设计得聪明不如让指令傻一点、整齐一点。RISC的典型特征是指令定长、格式规整、绝大多数指令只操作寄存器load/store结构、寻址方式少、寄存器数量多。这些特征刚好让流水线非常舒服指令定长取指和译码不用猜边界只操作寄存器不会在一条指令里又访存又运算流水线清爽很多。MIPS、ARM、RISC-V都是这个路数。代价是同样的程序代码密度比x86低一点编译器得更勤快。RISC的哲学是硬件做硬件擅长的事软件做软件擅长的事。6.3 真实世界没有纯血等你真去研究现代芯片会发现纯CISC和纯RISC几乎都不存在了。ARM在RISC内核上引入了Thumb-2变长指令来改善代码密度x86虽然对外是CISC内部分解成微操作之后再按类RISC的方式处理。当年的路线之争最终落到产品上全是折中指令集是知识产权的边界也是生态的边界但微架构怎么设计设计者已经灵活得很。这也是为什么我劝你别把“RISC好”或“CISC好”挂在嘴边真实硬件设计永远是各种力量综合作用的结果。理解这一点比记住哪个属于哪一派重要得多。7. 怎么把这些思想变成自己的应试与动手建议7.1 为什么背了定义还是不会做题我见过太多人复习组成原理抱着一本唐朔飞或者王道讲义从头背到尾名词解释滚瓜烂熟一做到计算题、设计题就傻眼。原因很简单组成原理考试考的不是名词是“过程”。它问你一个Cache的平均访问时间给的Cache命中率、命中时间、缺失代价你不是背出来的得会套公式它给你一条指令和数据通路问你哪个周期出什么信号你得能把数据通路完整画出来推演。所以复习的时候每学一个部件都不要孤立地记它的定义。建议把指令周期这条线作为主线每学一个新部件就问一句“它在指令执行的哪个阶段起作用”CPU、存储器、I/O全都能挂到这条线上之后知识就活了。7.2 亲手搭一个加法器比看十遍书管用如果条件允许强烈建议用Logisim这样的数字逻辑仿真软件亲手搭一次加法器。我个人的实操路径可以给你参考先用异或门和与门搭一个1位半加器再扩展成1位全加器然后级联成4位串行进位加法器观察最坏情况下的进位传播路径最后改造进位逻辑搭出4位超前进位加法器比较一下门数和延迟。组间串行进位的“组内并行、组间串行”这个考点你自己搭一遍就再也忘不掉了。做完加法器再往后可以搭一个简单的ALU、寄存器组然后用状态机做一个控制器雏形。这个过程比看十遍书都值。有开发板条件的话在FPGA上跑一个简易CPU或者拿51单片机做个带中断的小系统都会让你对硬件设计思想有真正体感。7.3 用软件模拟器验证你的理解没有仿真环境的同学可以用指令级模拟器来验证。比如MARS可以单步执行MIPS汇编你能看着PC怎么变、寄存器怎么被写、内存怎么被访问Ripes支持RISC-V单步调试还能看到管线阶段和寄存器的实时状态。边跑边对照教材里的数据通路图一番操作下来你对“取指-译码-执行”这条主线的理解会扎实很多。这些模拟器本质上都是“用软件模拟硬件行为”你别把它们当成玩具它们是硬件设计者验证设计早期想法的重要手段。最后再分享一点我自己的体会。学完整门课之后回头看计算机硬件设计思想其实就浓缩成三件事分而治之层次化模块化、接口先行ISA、ABI、总线协议、权衡取舍时间并行、空间并行、软硬件协同。这三件事放在你写代码、做架构、甚至项目管理的时候一样适用。所以学组成原理别只背结论每看到一个设计多问一句“它为什么这么设计、用什么代价换来了什么收益”这门课才真正长在你身上。
返回列表