
我们编写的每一行代码,最终都是对处理器的指令——现代技术的计算核心。然而,这个由硅构成的微观城市是如何将抽象的指令转化为具体结果的呢?在我们创造的软件与赋予其生命的硬件之间,通常存在着巨大的知识鸿沟。本文旨在通过深入探讨处理器的核心引擎——数据通路,来弥合这一鸿沟。通过探索这一基本概念,您将深刻理解计算过程在物理上是如何发生的。我们的旅程始于“原理与机制”一章,在这一章中,我们将剖析基本构件——数据通路、控制单元和功能单元——并审视支配它们操作的时序和流程,从简单的单周期设计到高效的流水线技术。随后,“应用与跨学科联系”一章将展示这套机制如何用于执行丰富多样的指令,揭示硬件设计与编程语言结构之间优雅的相互作用。
如果你能窥探微处理器的内部,你不会看到数字或指令,而会看到一个由微观开关和导线构成的、令人惊叹的复杂城市。处理器的核心任务,就是沿着预设的路径传输代表数据的电信号,并对它们进行转换。这个错综复杂的路径网络就是数据通路(datapath),是信息的物理道路和高速公路。但是,一个只有道路的城市若没有红绿灯和标志来引导交通,也是毫无用处的。这种引导来自控制单元(control unit)。数据通路和控制单元共同构成了处理器的核心,它们以一种精妙而迅捷的芭蕾舞般的方式来执行我们的程序。让我们拉开帷幕,理解使这支舞蹈成为可能的基本原理。
假设你有两个存储单元,寄存器 A 和寄存器 B。你希望能够根据指令完成两件事:要么让它们都保持当前值,要么让它们交换值。你会如何构建这样一个电路?
这不仅仅是一个异想天开的谜题,它正是数据操作的本质。我们需要一种方法来选择性地路由信息。让我们思考寄存器 A 的输入,我们称之为 。当我们想要“保持”(Hold)(假设我们的控制信号 为 0),我们需要 等于寄存器 A 的当前值 。当我们想要“交换”(Swap)(),我们需要 取寄存器 B 的值 。这个逻辑可以用一个简单的布尔表达式完美地捕捉到:。这是一个2-1 多路选择器的逻辑蓝图——一种根据控制信号从两个输入中选择一个的数字开关。这个不起眼的多路选择器是我们数字城市中最基本的构建模块之一。
当然,处理器不只处理单个比特;它们处理的是数据字(word)——通常是 32 或 64 个比特捆绑在一起,在我们称之为总线(bus)的路径上传输。如果我们想在两个 4 位总线 A 和 B 之间进行选择,我们不需要一个全新的、神奇的组件。我们只需将同样的原理进行扩展。我们取四个 2-1 多路选择器并将它们并排排列,每个比特一个。同一个控制信号 被发送到所有四个多路选择器。如果 ,所有四个都从总线 A 中选择它们各自的比特。如果 ,它们都从总线 B 中选择。瞬间,输出总线上的整个 4 位字就成了 A 或 B 的完美副本。这种优雅的并行性正是数据通路用简单、可重复的逻辑来管理宽数据流的方式。
现在我们有了道路(总线)和交叉口(多路选择器),我们可以开始建设一个功能性的城市了。数据通路不仅仅是移动数据,它还要转换数据。这由称为功能单元(functional units)的专门逻辑块完成。让我们考虑一个实际的任务:计算分支指令的“跳转”位置,这是 if 语句等编程逻辑的基石。
在许多体系结构中,一条分支指令可能会说:“如果两个寄存器相等,就向前跳转一定的偏移量。”处理器必须计算出这个目标地址。这涉及到取下一条指令的地址(我们称之为 )并加上分支指令本身提供的偏移量。但这里有个问题:为了节省空间,指令可能只将偏移量存储为一个 16 位数,而处理器的地址是 32 位的。我们不能直接将一个 16 位数加到一个 32 位数上,结果将是无意义的。
在这里,我们的数据通路需要两个特定的功能单元协同工作。首先,一个符号扩展单元(Sign-Extension unit)接收 16 位的有符号偏移量,并智能地将其扩展为 32 位数,同时保留其符号(无论是正还是负)。然后,一个 32 位加法器(Adder)将这个新扩展的偏移量与 的值相加。瞧,我们就得到了 32 位的分支目标地址。注意我们做了什么:我们用专门的组件——一个符号扩展器和一个加法器——组装了一台小型的、专用的机器,它们由数据通路连接,全部只为了执行一个关键的计算。整个处理器就是这样精心安排的功能单元的集合。
到目前为止,我们所拥有的是一幅静态的图景,就像一张城市地图。但城市是活的,交通以协调的节奏流动。处理器的节奏是它的时钟(clock)。时钟的每一次滴答都标志着计算进入新的一步。执行一条指令不是一个单一的事件,而是一系列更小的步骤,或称微操作(micro-operations),在几个时钟周期内精心编排而成。这个序列被称为指令周期(instruction cycle)。
让我们追踪第一个阶段:从内存中取指令。这并不像“去取它”那么简单。这是一个涉及几个关键寄存器的精巧舞蹈:
一个快速高效的取指过程分三步进行:
PC+4),让 PC 为下一次取指做好准备。这是一种简单的并行形式。这些步骤中的每一步都是我们控制单元生命中的一个状态。被设计为有限状态机 (FSM) 的控制单元,在每个时钟滴答时从一个状态转换到下一个状态,发出该步骤微操作所需的精确控制信号(如“加载 MAR”或“启用 ALU 加法器”)。它是指挥家,而时钟是它的指挥棒。
多周期方法看起来很合乎逻辑,但早期的设计者尝试了一种更简单的方法:单周期处理器。其思想是在一个非常长的时钟周期内完成一条完整的指令——从取指到最终结果。它的吸引力在于其简单性。但它隐藏着一种可怕的低效率。
单周期设计中的时钟周期长度由执行任何指令时信号必须传播的最长可能路径决定。这就是关键路径(critical path)。再次考虑 beq(相等则分支)指令。要在一个周期内执行它,必须发生一连串事件:
rs, rt)传到寄存器堆。PC+4 或计算出的分支目标地址)。这个长的依赖链——指令存储器 → [寄存器堆](/sciencepedia/feynman/keyword/register_file) → ALU → MUX——是这条指令的关键路径。时钟必须足够慢,以允许这整个马拉松式的过程完成。可悲的是,一条非常简单的指令,比如 ADD,可能有短得多的路径。但在单周期设计中,它也必须等待同样长的时钟周期。整个乐队被迫以其最慢成员的节奏演奏。这种束缚严重限制了性能。
我们如何摆脱关键路径的束缚?答案是计算机体系结构中最优美和强大的思想之一:流水线(pipelining)。其灵感直接来自工业装配线。与其让一个工人从头到尾组装一整辆汽车,不如将过程分解为多个阶段。当一个工人在安装轮子时,另一个工人正在为前一辆车安装引擎,而第三个工人则在为更前一辆车喷漆。
我们可以在指令执行中做同样的事情。我们将长长的数据通路分解为一系列阶段,由流水线寄存器分隔。一个经典的 5 级流水线可能是:取指 (IF)、译码 (ID)、执行 (EX)、访存 (MEM) 和写回 (WB)。
其中的奥妙在于:时钟周期不再由总时间决定,而是由最长阶段的时间决定。想象一个组合逻辑块需要 皮秒 (ps) 才能完成。非流水线设计的时钟周期至少为 ps。现在,如果我们插入一个寄存器,将逻辑分成两个阶段,一个耗时 ps,另一个耗时 ps,会怎么样?时钟周期现在由较长的阶段( ps)决定,再加上寄存器本身的微小开销(比如说,时钟到 Q 端的延迟和建立时间共 ps)。新的最小周期大约为 ps。我们几乎将时钟周期减半,从而有效地将时钟频率和指令处理速率翻倍!
在理想情况下,如果我们将一个任务分成 个完美平衡的阶段,我们可以实现 倍的吞吐量(throughput)——单位时间内完成的任务数量——的提升。当一条指令处于执行阶段时,下一条指令正处于译码阶段,而再下一条指令正在被取指。一旦流水线被填满,每个时钟周期都会有一个完成的指令产出。
这种性能的惊人提升是有代价的:复杂性和状态。流水线寄存器必须保存后续阶段所需的所有数据和控制信号。对于一个 5 级流水线,这包括指令本身、寄存器值、控制信号和计算结果,所有这些都从一个阶段传递到下一个阶段。处理器的总“状态”不再仅仅是 PC 和主寄存器;它是这些流水线寄存器中保存的所有比特的总和,这可以轻松达到数百比特。数据通路,由于这些寄存器的存在,已经转变为一个复杂的时序电路(sequential circuit),其输出不仅取决于当前的输入,还取决于流经其各个阶段的整个过去操作序列。
我们已经看到了数据通路的结构和其操作的节奏。但是,指挥这场交响乐的指挥家——控制单元——的本质是什么?历史上,两种哲学一直在争夺主导地位。
一种是硬布线控制(hardwired control)。在这里,控制单元是一个固定的、定制的 FSM。其逻辑是使用组合逻辑门直接蚀刻在硅片上的。它速度极快,因为控制信号是以电在门电路中传播的速度生成的。这是精简指令集计算机 (RISC) 的自然选择,其简单而规整的指令使得设计这样一个快速、定制的控制器变得可行。
另一种是微程序控制(microprogrammed control)。在这里,控制单元是一个微小的、原始的“计算机中的计算机”。对于每条机器指令,它从一个称为控制存储器的特殊、快速的内部存储器中执行一系列*微指令*。这种方法更灵活——你可以通过更新微码来修复错误甚至添加新指令。它曾是早期复杂指令集计算机 (CISC) 的救星,使其惊人的复杂性变得易于管理。然而,这种灵活性带来了性能损失;从控制存储器中取微指令本质上比直接的硬布线路径慢。
如今,界限已经模糊。摩尔定律的不断推进为设计者提供了如此多的晶体管,以至于他们可以兼得两者的优点。现代高性能处理器通常采用混合方法:常见、简单的指令用快如闪电的硬布线逻辑进行译码和执行,而罕见、复杂的指令则由微码引擎处理。
从一个多路选择器的简单选择,到一个流水线结构的宏伟策略,处理器数据通路是一个关于如何用优雅的方案解决基本挑战的故事。它证明了简单的原理——路由、转换和定序——如何能够组合成一台拥有几乎无法想象的力量和速度的机器。
在我们穿越了处理器数据通路的基本原理之后,你可能会有一种类似于学会了国际象棋规则的感觉。你知道棋子如何移动——寄存器、ALU、内存接口——但你还没有看到可以下出的那些优美而复杂的棋局。这些简单的组件,这些数字“棋子”,是如何组合起来执行构成所有软件基础的丰富多样的指令的?数据和控制信号的抽象舞蹈是如何产生从简单计算器到世界模拟的一切事物的?
在本章中,我们将探索这些原理的应用。我们将看到数据通路不是一个僵化、单一的实体,而是一个灵活、动态的舞台,在其上上演着一场宏大的计算交响乐。我们将发现,只需改变控制信号——我们逻辑门乐队的“乐谱”——同样的硬件就能执行一系列令人眼花缭乱的不同任务。然后我们将更进一步,探讨如何修改舞台本身,增加新的路径和专门的乐器,以扩展我们处理器的功能库。
计算机的核心是计算。让我们从最基本的操作开始:算术。考虑像 ADDI(立即数加法)这样的指令,它将存储在寄存器中的数字与一个直接编码在指令本身中的小常数相加。为了执行这个操作,控制单元扮演指挥家的角色。它将一个值从寄存器堆引导到一条数据总线上,并将指令中的立即数引导到另一条总线上。两条路径在算术逻辑单元 (ALU) 处汇合。然后,控制单元向 ALU 发出执行加法操作的信号,并最终将结果引导回存入目标寄存器。
现在,考虑一条 SUB(减法)指令。这需要一套全新的硬件吗?完全不需要!数据通路保持不变。唯一的变化在于乐谱:控制单元现在将两个值从寄存器堆引导到 ALU,并简单地告诉 ALU 执行减法而不是加法。这就是数据通路概念的深远优雅之处:一个统一的硬件结构只需接收不同的控制信号,就能执行各种指令。硬件是多功能的舞台,控制信号则为特定的表演编舞。
但计算不仅仅是算术。现代计算机的力量在于其决策能力。这种能力始于简单的逻辑问题。例如,slt(小于则置位)指令比较两个寄存器,如果第一个小于第二个,则在目标寄存器中置入值 1;否则,置入 0。再一次,数据通路基本相同。两个寄存器值被送入 ALU。但这一次,ALU 被指示执行比较操作。结果——不是和或差,而是一个表示真值的比特,一个 0 或 1——然后被送回寄存器堆。这个简单的操作是每个 if 语句、每个 while 循环、你的程序将做出的每个复杂决策的原子构建块。
一个只有 add、subtract 和 compare 的处理器功能会相当有限。为了构建更丰富的指令集,架构师经常添加新功能,这有时需要对数据通路进行细微——有时是重大——的修改。
想象一下我们想添加一条 SRA(算术右移)指令,这对于高效地进行 2 的幂次乘除法至关重要。我们的 ALU 可以被增强以执行移位操作,但一个新问题出现了:移位量从何而来?虽然一些架构可能使用来自另一个寄存器的值,但许多架构(如 MIPS)将一个小的移位量直接编码在指令字中。为了支持这一点,我们的数据通路需要一条新的“路径”。必须在 ALU 的输入端添加一个多路选择器,允许控制单元在来自寄存器的值(用于像 add 这样的指令)和来自指令本身的移位量字段(用于 SRA)之间进行选择。这阐明了一个基本的设计原则:增加功能通常意味着增加多路选择器来为数据流创建新的路由。
让我们更有野心一些。如何操作单个比特?像 BSET(位设置)这样的指令,它在寄存器中打开一个特定的比特,在系统编程和设备控制中非常有用。例如,BSET rt, rs 可能会在寄存器 rt 中设置由寄存器 rs 的低位比特给出的索引处的比特。这是一个复杂得多的操作。为了实现 1 << Register[rs][4:0],我们需要一个称为桶形移位器(barrel shifter)的专用硬件,它可以在一个周期内将一个数移动任意位数。这个新单元被添加到数据通路中。然后,为了执行最终的 OR 运算,数据通路必须以一种不那么明显的方式重新配置:rt 的值必须被路由到一个 ALU 输入,而新桶形移位器的输出则被路由到另一个输入。这就像在工厂装配线上增加一个专门的高精度工具,并配上将其集成到工作流程中所需的新传送带。
也许最优雅的增强之一是条件执行(conditional execution)的概念。通常,条件分支会改变程序的流程,这个过程可能很慢。一条 CMOVZ(零则条件传送)指令提供了一个巧妙的替代方案。它仅当先前计算的结果为零时(由 ALU 的 Z_flag 指示),才将一个值从一个寄存器复制到另一个寄存器。如果标志未设置,该指令什么也不做——它变成了一个“空操作”(no-op)。这完全避免了分支。其美妙之处在于其实现的简单性。最终的 RegWrite 信号,即启用对寄存器堆写入的信号,不再仅仅是来自控制单元的信号 (RegWrite_Ctrl)。相反,它由以下逻辑生成:,其中 CondWrite 是一个仅对我们的条件指令为 1 的新信号。这个简单的逻辑片段允许处理器的自身状态来控制其行为,这是构建更快、更高效代码的强大概念。
一个程序不是指令的随机集合,而是一个精心编排的序列。这个编舞的大师是程序计数器 (PC),即存放下一条待执行指令地址的寄存器。
大多数时候,舞蹈很简单:PC 只是指向内存中的下一条指令,地址通常是 。但是当我们遇到一个 if 语句时会发生什么?我们需要一个条件分支。数据通路计算一个潜在的目标地址,控制逻辑做出决定。一个简单的 AND 门,结合来自指令译码器的 Branch 信号和来自 ALU 的 Zero 标志,就可以决定结果。如果两者都为 1,PC 就采用新的目标地址;否则,它就顺序前进到 。令人惊奇的是,一个如此简单的机制竟能支配所有软件中复杂的自分支逻辑。
函数调用提出了一个更有趣的挑战。当我们跳转到一个函数时,我们还必须记住如何返回。这就是 JAL(跳转并链接)指令的目的。在一个流畅的动作中,数据通路执行两个关键操作:它将 PC 更新为新函数的地址,并且它将返回地址()保存到一个指定的寄存器中。为了实现这一点,必须开辟一条新的数据通路——一条从 PC 增量器到寄存器堆写数据输入的连接。这是一个完美的例证,说明了数据通路设计是如何直接物理体现编程语言结构需求的。
数据通路和内存之间的相互作用也可能变得相当复杂。一些在数组和数据结构处理中常见的指令,将内存访问与计算结合在一起。考虑一个假设的 lwpi(后增量加载字)指令,它从内存加载一个值,然后递增内存地址指针。在一个短时钟周期内完成这么多事情是不可能的。任务必须被分解为跨越多个周期的一系列步骤:
这种多周期方法揭示了真实处理器的资源限制——一次只能进行一次内存访问,一次只能进行一次寄存器写入——并展示了复杂指令是如何作为一系列更原始的“微操作”来执行的。
当我们掌握了基础知识后,我们就可以开始欣赏处理器设计的真正精湛技艺,硬件被精心雕琢以实现最高的性能和效率。
在数字信号处理 (DSP) 中,循环被执行数十亿次,典型软件循环的开销(「计数器减一」、「与零比较」、「非零则分支」)是令人望而却步的。为了解决这个问题,架构师创造了零开销循环(zero-overhead loop)指令。一条 LOOP 指令可能会原子性地递减一个计数器寄存器,检查其是否非零,并执行分支。实现这一点需要对数据通路和控制状态机进行仔细的修改,但回报是巨大的。这相当于音乐家将一段快速的琶音作为一个单一、流畅的手势来演奏,而不是一系列独立的音符。
这段进入数据通路的旅程也让我们接触到计算机算术的根本基础。ALU 究竟是如何执行乘法或除法的?这些操作并非魔法。它们是算法,和其他任何算法一样,但它们是直接在硬件中实现的。
从一条简单的 add 指令到一个可重构的算术单元,处理器数据通路的故事展现了涌现的复杂性和深刻的统一性。它证明了一小组简单而强大的思想——用多路选择器路由数据、用 ALU 转换数据、用控制单元对操作进行排序——如何能够被组合和扩展,创造出塑造我们世界的复杂而强大的计算机器。