ARM处理器与片上系统架构

ARM处理器与片上系统

本章内容关于中央处理器(CPU),即所有计算机核心的“心脏”。人们所称的“计算机体系结构”很大程度上指的是中央处理器的内部结构。更具体地说,本章介绍的是高级RISC机器(ARM)处理器,尤其是原始树莓派中使用的ARM11微架构。

本章对ARM11微处理器架构的关注引出了另一个主题:片上系统(SoC)设备,这类设备不仅包含ARM CPU,还集成了图形处理器、用于SD卡访问的大容量存储控制器、串口控制器以及多个其他子系统,而这些子系统过去通常作为独立的芯片或芯片组在中央处理器之外实现。

不可思议的微型中央处理器

早期计算机体积庞大是迫不得已;最初,数字逻辑基于高可靠性的电子管,每个电子管大约有拇指大小。需要专门设计的建筑物中的整个房间来容纳、供电并冷却成千上万个电子管。想象一下,一栋相当于现代服务器机房大小的建筑——如今可以容纳一排排多核刀片服务器——却只装有一个single中央处理器。

1955年商业化制造的晶体管问世,ushered in 了第二代中央处理器。新技术意味着以前占据整个房间的设备现在可以装入三到四个冰箱大小的机柜中。晶体管的尺寸仅为它们所取代的电子管的百分之一,所需功耗仅为电子管的千分之一。印刷电路技术使得计算机的大规模生产成为可能,尽管这里的“大规模”仅是相对而言。

IBM 其第一代基于电子管的701系统恰好制造了19台。几年后,IBM基于晶体管的1401售出了10,000台。数字设备公司(DEC)生产的原始PDP‐8机器只有冰箱的一半大小,销量超过50,000台。

20世纪60年代中期,随着集成电路的发展,第三代计算机技术到来。通过在单个硅芯片上最初集成少量晶体管,最终集成大量晶体管,计算机得以向两个方向发展:高端计算机(大型机)在物理尺寸上仍然较大,但计算能力得到了巨大提升;而低端计算机(小型计算机)则体积更小,价格更低,使得小型企业和学校也能负担得起。到1970年,PDP‐8的CPU机柜宽约半米,长不足一米,高仅30厘米。其外设(机械打印机、磁带和磁盘驱动器、电源等)使整个系统仍较为庞大,但CPU本身可以放在桌面,体积只比第一代个人计算机稍大一些。在整个产品生命周期中,PDP‐8系列共售出五十万台。

微处理器

尽管体积已经很小,但商用PDP‐8小型计算机的中央处理器仍然分布在多块电路板上,这些电路板塞满了独立的集成电路。(一种专用的单芯片版本出现在20世纪70年代中期,远在PDP‐8开始走向没落之后。)20世纪60年代末,硅制造技术持续进步,这得益于大型机计算机产业对固态存储芯片的无尽需求。到1970年,已经可以在单个硅芯片上制造2500个晶体管。这足以(勉强)容纳一个简单中央处理器所需的全部逻辑电路。由英特尔的费德里科·法金领导的一个团队设计出了4004微处理器,它成为首款商用大规模生产的单芯片中央处理器。

4004由于其4位数据字,如今被认为是一种奇特的存在;它主要用于桌面计算器。尽管如此,它的内存寻址能力(4096字节)与PDP‐8相同。它是英特尔发展其中央处理器帝国的起点。该公司迅速推出了8008、4500个晶体管、1972年、8080、1974年、8080、1974年。其设计影响了此后所有成功的英特尔中央处理器。8080成为公认的首台真正有用个人计算机Altair 8800的核心。

紧随8080之后,出现了数十种微处理器,其中一些取得了巨大成功:摩托罗拉的6800、齐洛格的Z80、RCA的COSMAC 1802系列(其抗辐射的蓝宝石硅变体被用于包括伽利略号在内的多个航天器),以及MOS科技的6502,后者被用于多款非常流行的个人计算机中,包括Apple II和最初的BBC微型计算机,而这直接促成了Acorn ARM处理器的发展。

在1980年之前,大多数早期的微处理器都处于摩托罗拉和英特尔的阴影之下。拥有3万个晶体管的8086(及其低价版本8088)通过IBM PC将个人计算引入商业领域。拥有5万个晶体管的68000则推动了首批图形用户界面(GUI)计算机的发展,包括Sun和Apollo工作站,以及后来的Apple Lisa和Macintosh。随着发展,摩托罗拉和英特尔的微处理器架构互为竞争对手,但摩托罗拉的68000架构难以与英特尔的中央处理器竞争,到1990年代中期便逐渐退出使用。到2006年,苹果电脑公司已在Macintosh产品线中采用英特尔处理器,英特尔由此成为个人计算领域的主导者。

到2016年,英特尔的Haswell‐E CPU包含26亿个晶体管,而高端至强服务器芯片可超过二十亿个晶体管。英特尔的“Knights Corner”至强Phi超级计算机组件处理器更是包含了惊人的七十亿个晶体管。

晶体管预算

这些数字不仅仅令人震惊。晶体管数量以根本性的方式影响了微处理器架构的演进。例如,任何CPU设计都始于一项工程研究,用以确定硅晶圆的尺寸以及晶体管的制造尺寸。这会在实际晶圆布局进行之前,就提前确定晶圆的最大晶体管数量。

在晶体管总数确定后,这些晶体管将被分配给构成中央处理器的各个组件功能:一定数量的晶体管用于缓存,一定数量用于寄存器,一定数量用于实现机器指令,以此类推。子系统设计团队对这些“晶体管预算”的保护,就如同政府或企业对其财务预算的保护一样严格。

最终的CPU设计总是在设计者希望“购买”的特征与他们所拥有的晶体管预算限制之间做出的权衡。如果你问一位CPU设计师为何某个理想的特征未能进入最终的硅片实现,答案几乎无一例外是:“我们没有足够的晶体管预算来实现它”。

数字逻辑基础

第3章解释了计算机将数据存储为二进制1和0的模式,这些模式通过导线上的电压存在或不存在来表示。本书不涉及数字逻辑设计的全部内容,但我们将在此回顾一些有助于理解中央处理器内部工作原理的基本概念。

逻辑门

数字计算机中的所有计算均由逻辑门执行,这些逻辑门接收一个或多个二进制输入,并生成(通常为)一个二进制输出。最基本的四种逻辑门是非、与、或和异或。这些逻辑门及其真值表如图4‐1所示,真值表总结了每种可能的输入组合所对应的输出值。每种类型的门电路都有一个符号表示,该符号用于多门逻辑电路的原理图中。

芯片设计人员可以使用单元库来构建更复杂的电路。现代互补金属氧化物半导体(CMOS)单元库包含数百个单元,这些单元实现具有多个输入的更复杂功能,但所有这些更复杂的CMOS功能的核心都是由NMOS(N沟道金属氧化物半导体)晶体管和PMOS(P沟道金属氧化物半导体)晶体管构成的。当NMOS晶体管的门电路输入为高电平时(即+V,无论何种情况),晶体管导通。

当PMOS晶体管的栅极输入为低电平或0伏特(通常称为地)时,PMOS晶体管导通。因此,NMOS和PMOS晶体管在导通方式上是互补的。我们可以使用一个NMOS和一个PMOS晶体管构成基本的CMOS非门(通常称为反相器),如图4‐2所示。

当输入端施加高电平(二进制1)时,N沟道金属氧化物半导体晶体管导通,将输出拉低(二进制0)。当输入端施加低电平(二进制0)时,P沟道金属氧化物半导体晶体管导通,将输出拉高(二进制1)。

所有逻辑门都会引入特征延迟,即输出对一个或多个输入变化做出响应所需的时间。如果将简单的门电路依次连接(即前一级的输出连接到下一级的输入)以实现更复杂的功能,则复合电路的延迟由从输入到输出的最长路径上的各段延迟之和决定。这被称为逻辑路径的传播延迟。

触发器和时序逻辑

现在你已经知道如何构建具有任意输入的组合函数(即通过组合更简单的逻辑门创建的函数),但要构建计算机,还需要能够构建具有状态(内存)并能随时间改变状态的系统。第3章提到双稳态触发器是简单SRAM(静态随机存取存储器)单元中的存储元件。D型触发器是用于在计算机内部保存状态的理想存储元件;参见图4‐3。

D型触发器在其时钟输入端检测到低电平到高电平的跳变(上升时钟沿)时,会捕获D输入端的状态,并将其呈现在Q输出端,直到下一个时钟边沿到来。通过将存储状态的D型触发器与组合逻辑电路相结合,可以构建复杂的系统,该组合逻辑电路根据当前状态和(可选的)外部输入来计算下一个状态。

图4‐4展示了一个简单的例子。假设你已经构建了一段用于对四位二进制数加1的组合逻辑,那么你可以实现一个计数器,在每次时钟滴答时,将存储在四个触发器中的四位数值递增。最高时钟频率由通过组合逻辑云的最长路径决定:你需要在触发器中的值发生变化后做出响应,并在下一个时钟边沿到来之前准备好新的值。

另一个有用的例子是移位寄存器,如图4‐5所示。移位寄存器在触发器链中逐位传递数据,每个时钟边沿推进一个位置。

本章中您所看到的一切都是这些基本原理的延伸:组合逻辑和用于存储数字状态的D型边沿触发触发器。

中央处理器内部

如第2章简要解释的那样,计算机程序只是一系列非常小的步骤。每一个非常小的步骤称为一条机器指令,它是“原子的”单元。

触发器:位的存储之地

触发器是一种存储逻辑状态的电子电路,通常描述为1或0。一旦通过输入端的数字信号(通常是电压从0伏特变为5伏特或从5伏特变为0伏特)将其设置为特定状态,该触发器将保持该状态,直到另一个输入信号改变它为止。由于触发器可以存储两种逻辑状态之一,因此有时被称为双稳态。触发器有多种不同类型,但在计算机逻辑中最常用的是D型,其中D代表“数据”。存储在触发器中的1和0状态可用于表示计算机数据,因此得名。

在中央处理器之外无法再分割的操作。每个中央处理器系列都有其独特的机器指令列表。这些指令可能实现类似的功能,但通常情况下,一个中央处理器系列的机器指令无法在另一个中央处理器系列上执行。中央处理器的机器指令及其功能的定义称为其指令集架构(ISA)。

一条指令在内存中由一个长度为若干字节的二进制值表示。(在许多32位CPU上,例如原始树莓派中的ARM11,这个长度是四个8位字节。)在此二进制数中编码了指令的标识(称为操作码或操作码),以及一个或多个操作数,这些操作数是指令相关联的数值或地址。一条二进制机器指令从内存加载到中央处理器中,中央处理器对其进行解码(将其分解以确定需要执行的操作),然后执行该指令,在执行过程中完成指令的实际工作。当一条指令被派发执行时,称其已被发出,当它完全执行完毕后,称其已退休。接下来的程序指令被加载到中央处理器中以供执行。(在现代CPU中,此过程比上述描述更为复杂,我们将在本章后面进行解释。)

从高处看,中央处理器执行程序的过程如下:
1. 获取程序中的第一条指令。
2. 解码指令。
3. 执行指令。
4. 获取下一条指令。
5. 解码指令。
6. 执行指令。
……依此类推,直到程序中的所有指令执行完毕。程序计数器是中央处理器内部的一个指针,其中包含当前正在执行的指令的内存地址。

机器指令可以执行诸如
- ADD、减法、乘法或除法
- 对二进制值执行与、或、异或和非等逻辑运算
- 将多比特二进制值向左或右移位
- 将数据从一个位置复制到另一个位置
- 将数值与常量或其他数值进行比较
- 执行CPU控制功能

机器操作的数值可能来自外部内存,或来自中央处理器内部数量相对较少的寄存器之一。寄存器是一种存储位置,能够同时保存多个位;通常为16、32或64位,具体取决于中央处理器。机器指令操作的结果可以存储到内存或寄存器中。

在现代CPU中,不同的子系统执行不同的机器指令组:
- 算术逻辑单元(ALU):负责处理简单的整数运算和逻辑运算
- 浮点运算单元(FPU):处理浮点运算
- 单指令多数据(SIMD)单元:处理向量运算,可同时对多个数据值执行操作。此类运算在音频和视频应用程序中至关重要。

现代高性能中央处理器可能包含多个每个单元的副本,以支持指令的并行执行,这一点我们稍后会解释。

跳转和标志位

尽管执行线性指令序列可能很有用,但计算的真正魔力在于程序能够根据其工作结果改变执行流程。这是通过分支指令实现的,这些指令有能力在构成程序的机器指令序列中向前或向后跳转。某些分支指令——称为无条件跳转指令——会告诉中央处理器“直接跳转”,并从跳转指令中包含的内存地址加载下一条指令。

条件分支指令将某种测试与分支操作结合起来。这些测试通常涉及一组称为“标志位”的单比特二进制值,它们存储在中央处理器中的某个位置,通常位于一个称为标志寄存器或状态字的寄存器组中。当某些机器指令执行时,它们会设置(更改为二进制1)或清除(更改为二进制0)一个或多个标志位。例如,所有中央处理器都具有比较两个寄存器值的指令。如果这两个值相等,则一个标志位(通常称为零标志位)被置为1;如果这两个值不相等,则该标志位被清零。这个标志位被称为“零”标志位,原因在于比较操作的工作方式:为了比较两个寄存器,中央处理器会从其中一个寄存器中减去另一个寄存器的值。如果减法的结果为零,则说明它们相等,零标志位被设置;如果减法的结果非零,则说明这两个寄存器不相等,零标志位被清除。

机器指令只是一种二进制数。尽管可以直接用机器码编程,但为了方便起见,程序员通常使用汇编器将汇编语言直接转换为机器指令。汇编语言中的指令由称为助记符的短字符串表示,各种操作数则以相应形式书写可读形式。条件分支机器指令的汇编语言表示可能如下所示:

BEQ [地址]

该指令的作用是,如果相等(即零标志位被置位),则分支到存储在指定内存地址的机器指令;如果零标志位未被置位,则继续执行内存中的下一条指令。

中央处理器的架构中可能有十几个或更多的标志位。一些标志位反映相等性,或者寄存器的值已变为零。一些指示是否发生了算术进位。一些指示寄存器是否被设置为正值或负值。一些指示错误条件,例如数值溢出或尝试除以零。一些反映中央处理器内部机制的当前状态。对于每个标志位,都有一条或多条分支指令用于检查该标志位的值,并相应地进行跳转。

除了支持条件分支指令外,树莓派所使用的 ARM CPU 在其指令集中还具有一种更为通用的条件执行特征,这将在后面进行较为详细的描述。

系统栈

计算机科学家记录和描述了相当多的数据结构,包括数组、队列、列表、栈、集合、环和包等。其中一些数据结构使用非常频繁,以至于某些中央处理器在其机器指令中提供了对它们的硬连线支持。其中最重要的是栈。

栈是一种后进先出(LIFO)的数据存储机制,对包括树莓派的ARM11在内的大多数现代CPU的运行至关重要。栈操作的关键特征是,数据项从栈中移除的顺序与其存储的顺序相反。

一个比喻很好地描述了这一点。如果你曾在学校食堂用餐,可能见过一种存放餐盘和碟子的常见装置:一个位于金属圆筒内的弹簧加载平台,其设计可平衡所承载餐盘的重量。当你把一个餐盘放入圆筒时,平台会刚好下移,为下一个餐盘腾出空间。当你需要取用餐盘时,只需从圆筒顶部拿走最上面的一个。由于负载减轻,平台会上升恰好的距离,使下一个餐盘升至圆筒顶部。

圆盘存储筒的关键在于,放入筒中的第一个圆盘位于最底部,而最后一个放入的圆盘则位于顶部。最后存入的圆盘最先被取出——因此称为“后进先出”。

在计算机系统中,栈是内存中预留出来用于后进先出数据存储的区域,由旨在实现栈数据结构的机器指令进行管理。图4‐6展示了一个简单的栈。

栈从由基址指针指定的内存位置开始。(指针只是一个内存地址。)在加载一个地址后,基址指针的值不会改变。另一个称为栈指针的指针,指示下一个要访问的内存地址,有时也被称为“栈顶”。在图4‐6中,栈顶的项目被加了阴影。

要将项添加到栈中,首先将栈指针递增,使其指向栈中的下一个可用内存地址。然后将数据项写入该位置。通常,这被称为将项压入栈。

要从栈中移除一项,首先将栈顶的项复制到一个寄存器或内存中的其他位置,然后将栈指针递减,使其指向之前栈中的顶部项。这个过程称为从栈中弹出项。如果你查看图4‐6中的四个栈快照,就可以看到当项被压入栈或从栈中弹出时,栈是如何增长或缩小的。最后被压入栈的项会最先被弹出——记住,后进先出。

在任何特定架构中,栈的实现方式存在一些差异。如上所述,升序栈在每次压栈时通过将栈指针递增至下一个更高的内存地址而在内存中向上增长。而降序栈在每次压栈时通过将栈指针递减至下一个更低的内存地址而在内存中向下增长。ARM CPU的栈可以配置为任一种工作方式,但按照约定,ARM栈通常为降序栈。某些架构假定栈指针指向栈中第一个空闲的内存地址,而其他架构则假定栈指针指向最近压入栈的最后一个项。如果栈为空,则栈指针始终指向第一个可用的栈位置。同样,ARM处理器可以配置为任一种方式,但默认情况下,ARM栈假定栈指针指向最近压入的最后一个项。

栈用于在子程序调用期间临时存储数据项(通常是寄存器值)和内存地址。子程序是程序中作为一组执行并被赋予名称的一系列操作。每当需要执行该子程序的操作时,程序的其他部分就可以调用它,即将执行转移到子程序,直到子程序的工作完成为止。然后子程序将执行返回到调用它的程序部分。在C和Python等编程语言中,子程序被称为函数。我们将在第5章中更详细地讨论子程序及其在编程中的作用。

许多计算机架构提供了一条专用指令用于调用子程序,该指令在跳转到子程序的起始地址之前,会自动将程序计数器的值压入栈中。当子程序执行完毕后,可通过另一条专用指令将保存的程序计数器(称为返回地址)从栈中弹出并恢复到程序计数器中,程序继续执行。如果子程序需要使用某个中央处理器寄存器(该寄存器很可能已被调用该子程序的程序所使用),则子程序可先将该寄存器的当前值压入栈中,在返回前再将其弹出恢复。

注意,尽管ARM CPU可以选择手动将子程序返回地址保存在栈上,但有一种更快的方法可以避免访问系统内存带来的时间开销。正如您稍后在本章中将看到的,返回地址首先存储在链接寄存器(LR)中,这使得一些叶函数(即那些不再调用其他函数的函数)完全可以避免栈访问。

栈在管理嵌套子程序调用(从子程序内部发起的子程序调用)方面非常有用。每次发生新的嵌套子程序调用时,栈中都会增加一层数据和返回地址。只要栈还有空间,就可以进行数十次甚至数百次的嵌套调用。如果栈已满,无法容纳更多值,则尝试向栈中压入数据将导致栈溢出。如果没有相应的保护机制(例如来自内存管理单元的保护),栈相邻内存区域中的数据就会被覆盖,从而导致程序故障。

系统时钟与执行时间

如前面“数字逻辑基础”部分所述,中央处理器等时序电路内部的所有操作都与一个称为时钟的脉冲发生器同步。时钟发出的每一个脉冲触发一个时钟周期,在该周期内中央处理器完成一些特定的工作。在非常早期的中央处理器中,一条机器指令可能需要4到40个时钟周期才能完成执行。不同的指令所需时间不同,其中一些(如乘法和除法指令)比其他指令耗时更多。

为什么不同的指令需要更多的时间?在计算技术发展的早期几十年中,机器指令是在CPU硅片内通过一系列微指令实现的,这些微指令是非常简单的微型步骤,复杂的指令可以通过组合这些微指令构建而成。(微指令无法从CPU外部访问。)通过使用数量少得多的微指令来组合实现大量机器指令,微指令节省了CPU芯片上的空间。因此,实现指令的数字逻辑可在许多指令之间共享,从而减少了所需的总晶体管数量。执行每条指令所需的微指令列表称为微码。

使用微码实现的机器指令执行会显著增加指令执行时间。只要可能,CPU设计者都会采用硬连线方式实现指令;也就是说,他们使用专用于单条指令的晶体管逻辑电路直接实现每条指令。这种方式比微码需要更多的晶体管预算和芯片空间,但能产生更快的指令执行速度。随着单个芯片上可容纳的晶体管数量不断增加,越来越多的指令被硬连线实现,而依赖微码的指令则逐渐减少。即便如此,直到最近之前,由于使用微码,某些指令完成所需消耗的时钟周期仍多于其他指令。图4‐7展示了在因微码而导致指令执行较慢的早期计算机上的工作情况。

以及除法。当所有机器指令都采用硬连线实现时,所有指令的执行时间几乎相同。

CPU架构中的终极目标一直是实现所有机器指令在一个时钟周期内完成执行。到2000年左右,这一目标基本已经实现,机器指令与时钟周期的关系图也演变为类似于图4‐8的形式。

图4‐8 可能会让你认为指令执行速度已经达到了瓶颈,唯一能提高每秒执行指令数量的方法就是提升时钟速度。但你是错的。

流水线

关于中央处理器操作和时钟速度存在一种误解:中央处理器的运行速度并不完全由时钟速度决定。时钟速度只能达到中央处理器所能允许的最快程度。中央处理器需要一定的时间来完成其操作。

如果你仔细观察中央处理器执行一条机器指令的过程,会发现它发生在若干相对独立的阶段:
1. 从内存中获取指令。
2. 解码指令。
3. 执行指令。
4. 将指令对寄存器或内存的更改写回。

当一条机器指令在一个时钟周期内执行时,所有四个阶段会在一波晶体管活动中完成。这一波活动从负责取指和指令译码的逻辑电路开始,经过执行阶段,最终到达写回逻辑。要让这波活动进行得更快非常困难,而最高时钟频率将由信号通过所有这些逻辑电路中最长路径所需的时间决定。

然而,由于这四个阶段按特定顺序发生,你可以将每个阶段视为独立的操作。如果你能设计出执行机器指令的逻辑电路,使得所有四个阶段大致花费相同的时间,就会出现一个有趣的可能性:你可以让它们重叠执行。参见图4‐9。

在图4‐9中,每条指令执行的每个阶段耗时一个时钟周期。这意味着时钟可以运行得更快,因为现在执行一条指令需要四个时钟节拍,而不是一个。即使时钟频率翻倍,这听起来也像是性能上的倒退。事实上,起初这看起来像是一个悖论:完成任何单条指令都需要四个时钟周期,但每条指令在一个时钟周期内被发出,同时另一条指令也在该周期内退出(即完成其工作)。最终结果是,指令仍然相当于在一个更快速的单一时钟周期内执行。

要理解这一点,可以想象一下你在某些披萨柜台后方看到的传送带式披萨烤箱。厨师将一个生披萨放在烤箱入口处的传送带上。十分钟后,披萨从烤箱中出来,已经完全烤好,可以出售了。烤一个披萨需要10分钟。然而,在任何时候,烤箱中都可以有五个披萨正在通过,假设厨师持续不断地放入生披萨。

传送带上的生披萨,每两分钟会有一份完成的披萨从烤箱中出来。第一个披萨需要10分钟。但一旦烤箱满了,每两分钟就完成一个披萨。

以这种方式重叠执行机器指令称为流水线。流水线最初在1980年代的超级计算机中实现,如今几乎已成为所有中央处理器的标准配置,甚至包括微芯科技的低成本PIC(可编程智能计算机)微控制器。在近年来对CPU性能提升的贡献方面,流水线仅次于内存缓存。

流水线详解

为了了解流水线的工作原理,先来看一个如图4‐10所示的简单假设的非流水线处理器。触发器保存处理器的当前状态(即当前程序计数器(PC)和寄存器),而一个逻辑云计算出下一个状态,并在下一个时钟边沿到来之前将其送入触发器的D输入。

你可以大致将这个逻辑云分为三个部分:指令取指(IF)、译码(DC)和执行(EX)。在IF部分中,有一些逻辑电路用于计算下一个程序计数器(PC)的值——在这个假设的处理器示例中没有分支。寄存器直到EX阶段才需要使用。在每个周期开始时,一些触发器的输出发生变化,在周期内,一股活动波从左到右穿过逻辑云。最高时钟频率由信号穿越逻辑云中最长路径所需的时间决定。在周期的后半段,逻辑云左侧的部分已达到稳定状态,仅向右侧仍在变化的逻辑电路提供结果。如果能对这一稳定状态进行快照,并让左侧部分转而处理其他任务(例如取指下一条指令),岂不是很好?流水线处理器正是通过在逻辑云中插入流水线锁存器(同样是触发器)来实现这一点的。

图4‐11 展示了一个带有流水线锁存器的处理器。在图中,我们将逻辑云划分为三个子云。IF 云只需从内存中获取指令,并在第一组流水线锁存器记录结果之前确定下一个 PC 值。然后,它可以在下一个周期继续取指,同时 DC 云逻辑使用流水线锁存器中的数据作为输入来解码前一条指令。寄存器读/写操作全部在 EX 阶段完成,因为我们希望能够在某个周期内向寄存器文件写入一个值,并在下一个周期使用该值。

中央处理器的速度再次由穿越云中任意部分最长路径所需的时间决定,但由于我们将云切分为三个部分,因此曾经的最长路径必然比图4‐10所示的非流水线处理器更快。

看到这一点,你可能会觉得执行阶段(EX stage)有点“满”。所有重要的部件,特别是算术逻辑单元(ALU),都集中在此。事实确实如此:在这样简单的流水线中,执行阶段往往包含最长路径,从而限制了流水线的性能。下一步的逻辑发展——正如你在下一节的ARM11中所见到的——是将执行阶段划分为多个更小的阶段。这反过来要求你解决寄存器文件在不同流水线阶段被读取和写入时所产生的问题。

深的流水线与流水线冲突

在给定的中央处理器中能够实现多少重叠,主要取决于该中央处理器的指令执行可以被分解为多少级。早期,3级和4级流水线是先进技术。正如你稍后将看到的,原始树莓派内部的ARM11 CPU具有8级指令流水线,而当前许多英特尔处理器的流水线达到20级或更多级。对于考虑更长流水线的CPU设计人员来说,一个挑战是指令执行的不同阶段所需时间并不相同:由于每个阶段需要一个时钟周期来完成,因此控制中央处理器操作的时钟周期长度取决于最慢的流水线阶段所需的时间。

以持续且均匀的速率将指令通过流水线至关重要。某些因素可能会扰乱指令在CPU流水线中的顺畅流动,这些因素称为流水线冒险,并可能导致流水线延迟。这些延迟称为流水线停顿。流水线冒险通常分为三类:
- 控制冒险:由条件分支指令引起
- 数据冒险:由指令之间的数据依赖引起
- 结构冒险:由资源冲突引起

很容易看出条件分支如何打乱流水线。如果图4‐9中流水线所示的第一条指令是一条条件分支指令,并且(通常情况如此)判断是否执行跳转的逻辑位于执行阶段,那么可能会导致跳转到已经进入流水线并被取指和解码的顺序指令之外。这些指令将不再处于程序执行路径中。为了保持逐条执行指令的假象,就需要丢弃这些指令,并从分支目标地址开始重新填充流水线中的指令。回想一下披萨烤箱的比喻,如果下单员向厨师提交了错误的订单,一个或多个已经在制作过程中的披萨。

通过烤箱的披萨可能需要丢弃,并在传送带上重新布放替换品。这会导致在新的有效披萨从烤箱中出现之前产生停顿,更不用说整体吞吐量的损失了。

一种应对控制冒险的历史方法是放弃逐条执行指令的假象,转而采用延迟分支:在分支被解析时,已进入流水线的后续指令总会被执行,无论分支是否被采纳。然后由汇编语言程序员或高级语言编译器来填充这些分支延迟槽中的有用工作。

然而,这种行为并不常见。大多数架构通过两种相互关联的机制来减轻流水线冒险的影响:分支预测和推测执行。在此过程中,中央处理器的执行逻辑会尝试预测两个可能的分支目标中哪一个将被采用。该预测基于代码该部分已执行分支的累积历史。在实际分支结果确定之前,中央处理器会从更有可能的目标处预取指令,并开始对其进行推测性执行。当预测错误时,需要在这些推测执行的指令到达可能影响外部世界阶段之前将其取消,通常通过用气泡(空操作指令,不执行任何操作)替换它们来实现。推测执行相当于中央处理器进行某种猜测,而错误的猜测代价高昂,其延迟大致与流水线深度成正比,因为流水线需要时间重新填满。在现代高性能处理器中,20个时钟周期的延迟并不罕见,因此分支预测器的改进已成为决定CPU性能的主要因素。

数据依赖更为微妙。假设流水线中一条指令的结果值被下一条指令作为操作数使用,第二条指令可能在第一条指令完成生成该值之前就需要用到它。如果不阻止第二条指令继续在流水线中前进,它最终会使用一个垃圾值,或是之前某次计算的残留值。这种情况在前面描述的简单流水线处理器中不会发生,因为寄存器的读取、结果的计算以及写回操作都发生在执行阶段(EX stage)。当下一条指令到达执行阶段时,寄存器的内容已经完全一致。只有当你开始将过长的执行阶段拆分(几乎所有现代处理器,包括ARM,都会这样做)时,才需要担心这个问题。

当流水线中的两条指令需要同时访问某个CPU资源时,就会发生资源冲突。例如,如果处于不同流水线阶段的两条指令需要同时通过缓存系统访问外部内存,则其中一条指令必须优先于另一条。一个简单的例子是取指阶段(IF阶段)读取指令,而其他某个流水线阶段(在我们的简单示例中为执行阶段EX阶段)读取或写入数据。这种特定的冲突可以通过将统一的一级缓存拆分为两个分离缓存来部分解决:一个用于数据,另一个用于机器指令。这被称为改进型哈佛架构,得名于哈佛大学早期的实验性计算机。分别存储和访问机器指令与数据。ARM11 CPU 采用改进型哈佛架构。

检测和解决数据依赖与资源冲突风险需要在硅片上使用更多的晶体管来实现。通常的方法是通过指令解码逻辑识别流水线中即将发生的危险;执行此检查的硬件被称为互锁。如果取指的指令存在任何类型的危险,则会在该问题指令之前向流水线中插入一个气泡(流水线停顿)。这会产生一段延迟,使得前面的指令能够在后续指令进入流水线之前完成其操作,从而避免冲突。

ARM11流水线

ARM11 CPU中的流水线被分为八个阶段,如图4‐12所示。该流水线并不像图4‐9中所示的那样简单。除了流水线被划分为八个不同的阶段外,还存在三条可能的流水线路径。执行所采取的路径取决于正在执行的指令类型。

前四个阶段无论指令如何都是相同的。然而,当指令被发射时,解码逻辑会选择三条可能路径中的一条。每类指令都有其自己的流水线路径:
- 整数执行路径:对于大多数执行整数操作的指令
- 乘累加路径:用于整数乘法指令
- 加载/存储路径:对于加载和存储指令

图中所示的阶段及其缩写为:
- FE1:取指阶段的第一阶段;请求指令的地址并接收指令。
- FE2: 分支预测在此阶段完成。
- 解码:指令被解码。
- 发射:读取寄存器并发出指令。
- 移位:本阶段执行任何所需的移位操作。
- ALU: 此阶段在算术逻辑单元中执行所需的整数操作。
- Saturate: 整数结果被饱和处理;即强制使其落在整数范围内。
- MAC1: 乘法指令执行的第一阶段。
- MAC2: 乘法指令执行的第二阶段。
- MAC3: 乘法指令执行的第三阶段。
- WBex: 指令所更改的任何寄存器数据都会被写回寄存器。WBex 是整数执行路径和乘累加路径上的最后一个阶段。
- 地址:用于生成指令访问内存时使用的地址。
- DC1: 数据缓存逻辑处理地址的第一个阶段。
- DC2:地址由数据缓存逻辑电路处理的第二个阶段。
- WBls:加载/
存储路径的最后阶段将对内存地址所做的任何更改写回。

使情况更加复杂的是,整数执行路径和乘法累加路径由整数执行单元处理,而加载/存储路径则由独立的加载/存储单元处理。执行单元是一种CPU子系统,负责处理指令的“工作”,即整数运算或逻辑运算、内存访问等。如果核心中存在浮点协处理器,则在指令发出后,由协处理器自身的流水线(此处未显示)来处理执行。(我们将在后面的“协处理器”一节中更详细地解释协处理器。)

超标量执行

事实证明,流水线技术仍能进一步榨取更多性能。一种称为超标量执行的机制在1980年代末期出现。超标量架构具有类似前一节所述的指令流水线,几乎所有的中央处理器都具备此类设计。然而,超标量CPU会同时发出多条指令进行执行。一旦发出,这些指令将同时执行。采用超标量CPU后,指令的执行超越了重叠,实现了真正的并行性。图4‐13展示了 一个超标量流水线。

在这种简单的情况下,超标量CPU从内存中提取两条指令,并检查它们是否可以并行执行。如果可以,CPU便将这两条指令的执行分配给双执行单元。这些执行单元不是完整的处理器核心,仅负责处理指令工作,并专门用于整数运算和逻辑电路、浮点运算以及向量运算。CPU力求尽可能长时间地保持所有执行单元处于忙碌状态。

基本机制与流水线相同:中央处理器会检查指令流中的数据依赖,例如某条指令是否为其后续指令提供数据值。如果存在此类依赖关系,则这两条指令不能同时发出,从而导致流水线停顿。例如,如果前一条指令向寄存器4添加一个值,而下一条指令将寄存器4的内容乘以另一个值,则这些指令无法同时发出并行执行,因为第二条指令依赖于第一条指令计算出的数据。

与流水线一样,生成程序代码的编译器能够查找数据依赖并重新排列指令,使得两条连续的指令之间不会以触发互锁的方式相互依赖;也就是说,避免一条指令在依赖于另一条指令提供数据的情况下超前执行。这些优化已变得不那么。

近年来变得重要,因为最新的超标量CPU允许乱序执行。这类CPU能够动态地重新排列输入的指令流,以最大化可实现的并行性,并最小化导致互锁的数据依赖。

超标量执行,尤其是乱序执行,在晶体管逻辑方面代价较高。除了需要提供重复的执行单元外,实现依赖性检查的逻辑也变得越来越复杂。理论上,中央处理器可以同时发射超过四条指令,但大约在此时,设计者通常会达到一个收益递减的临界点。ARM11微架构不支持超标量执行。超标量功能是在“Cortex A”系列处理器中引入ARM产品线的,其中一些处理器能够同时发射四条指令。(本章稍后会详细介绍Cortex。)

使用SIMD实现更多并行性

超标量执行难以实现但易于描述:多条指令同时发出,并并行执行。现代CPU支持另一种类型的并行性:同时对多个数据项进行操作的指令。这类指令统称为单指令多数据(SIMD)指令。大多数计算机架构都有各自的SIMD指令,这些指令通常与其他架构的SIMD指令不相同,甚至不兼容。

SIMD 最好通过一个例子来解释。在像 ARM11 这样的 32位 微架构中,普通的加法指令在一个操作中将一个 32位值 与另一个 32位值 相加。其他指令以相同方式执行减法。计算中的某些常见任务需要尽可能快地执行大量加法(或其他算术运算)。调整显示器上的颜色就是一个这样的挑战。如果你有一个 1600‐×‐1200 像素的显示器,你就需要处理将近两百万个像素。而且,每个像素都需要进行三到四次加法或减法来调整颜色。即使这些数学运算是简单且重复的,计算量仍然非常大。

使用传统的机器指令,进行所有这些加法和减法的唯一方法是一次一个(参见图 4‐14)。调整整组像素需要一个程序循环,每次迭代处理一个值。(我们将在第5章 中更详细地描述程序循环。)这样的循环每个值都需要一次分支,以及一条指令来加载值,另一条指令来写回修改后的值。

有一些技巧可以尽量减少此类循环中所需的分支数量,但这些技巧节省的效果有限,而且会带来额外的指令和额外的内存开销。如果你需要处理两百万个像素,这些开销会累积起来,而且并非有利。

SIMD指令旨在同时对多个数据值执行相同的操作。常规指令作用于标量(单个值),而我们说SIMD指令作用于向量。向量就是一维的数据值数组,其排列方式使得特定架构的SIMD指令能够对其执行操作。向量的长度通常为2到16个数据值,每个值的宽度(位数)因架构而异。

在许多计算机架构中,一条SIMD指令可并行地同时执行四个操作(加法、减法、乘法和除法)。在某些计算机架构中,操作数可能超过四个,但原理相同:从内存中加载一个包含四个值的向量到寄存器中。一条SIMD指令同时对向量中的这四个值执行操作,然后将整个向量写回内存。图4‐15说明了这一点。

原本需要四次独立的加法或减法操作,现在仅需一次即可完成,节省了三个时钟周期。更优的是,在大多数架构中,还有相应的SIMD指令可以一次性加载和存储四个内存值。

为什么构建SIMD机器而不是增加处理器的超标量发射宽度,让程序员继续使用对标量进行操作的指令?SIMD的关键优势在于,取指和解码一条SIMD指令的成本(在时间和能量方面)可以分摊到多个计算上。由于程序员通过使用SIMD指令显式声明这些计算是相互独立的,因此无需使用昂贵的互锁逻辑来检测和规避现在不可能发生的依赖关系。

对于初学者来说,SIMD指令的用途并不立即明显。但事实证明,声音和图形(尤其是3D图形和视频)的数学运算需要对长序列的数值进行大量重复计算。SIMD指令可以同时对长序列的数值执行数学运算。SIMD指令可以显著提升处理声音和视频编解码以及管理3D图形等任务的代码性能。

原始树莓派中的ARM11核心以有限的方式支持SIMD指令执行:32位数据字照常加载,但SIMD指令将字内的4个字节各自视为独立的值。这显然限制了可使用SIMD处理的数值大小,尽管在图形和音频处理中大量操作可用8位数量完成。

在较新的 ARM Cortex CPU 中,有一个名为 NEON 的协处理器,它提供 SIMD 指令,可对存储在特殊 128位寄存器中的多个数据进行操作。这使得吞吐量超过 ARMv6 指令集中的 SIMD 指令两倍以上。稍后在介绍 ARM Cortex 时,您可进一步了解 NEON。

字节序

首批面向大众市场的微处理器是8位单元,一次处理8位(1字节)的数据。它们也以每次1字节的方式从系统内存读取和写入数据。后来的中央处理器将这一宽度提升至16位,然后是32位,如今许多架构已能一次读取和写入64位。在一次读取或写入操作中从内存访问多个字节时,会引出一个不明显的问题:这些多个字节在内存中的排列顺序如何?

如果从内存中读取一个4字节或8字节的数据量,中央处理器如何解释这些字节?这个问题被称为字节序,其名称源于乔纳森·斯威夫特的小说格列佛游记中的一段隐晦讽刺:小人国居民激烈争论应该从鸡蛋较宽的“大”端还是较窄的“小”端敲开一个溏心蛋。这在计算机架构中是一个重要问题,即使在鸡蛋上并非如此。在讨论过程中,请参考图4‐16。

图4‐16显示了一段较短的计算机内存。每个位置都有一个地址,并存储1字节的数据。地址和数据值均以十六进制形式给出。像ARM11核心这样的现代32位中央处理器在每次内存访问时读取或写入4个字节。如果这4个字节表示一个32位数字,则需要知道这4个字节在该数字中的排列顺序。按照列式记法(参见第2章回顾),数字的最低有效列按约定显示在右,最高有效列显示在左。“最高有效”在此表示“值最高”。在32位。

二进制表示的值为20,或1。最左边的值为231,或2,147,483,648。(参见第3章的表3‐1。)顺序很重要!

在小端架构中,多字节值的最低有效字节存储在内存中这四个字节的最低地址处,而最高有效字节则存储在最高地址处。在图4‐16中,地址0x10000处的数据是0xE7。在小端系统中,值0xE7被解释为最低有效字节;而在大端系统中,值0xE7将是最高有效字节。这会极大地改变32位数字的值:在小端系统中,十六进制值 0x00 11 04 E7对应的十进制数为1,115,367;而在大端系统中,该十六进制数变为 0xE7 04 11 00,其对应的十进制数为3,875,803,392。

尽管深奥的技术问题更倾向于小端架构而非大端架构,但在大多数情况下,小端架构一直是一种约定。最近的大多数微处理器架构,包括英特尔的x86,都是小端序。(摩托罗拉的6800、68000和太阳微系统公司的SPARC是显著的例外。)像IBM历史悠久的System/360这样的大型机架构通常是大端序。

默认情况下,ARM11核心为小端模式。然而,自ARMv3以来的ARM架构提供了一个有趣的特征:可以根据需要将字节序配置为小端或大端。这被称为双端性。由于计算机网络按约定采用大端模式,允许中央处理器以大端模式解释网络数据可以带来性能提升,因为中央处理器无需重新排列数值的字节顺序。

字节序至关重要的另一个方面是数据文件。在内存中操作字节精度二进制数据的应用程序需要知道中央处理器是以大端或小端块形式将数据写入磁盘的。如果数据文件被迁移到使用不同字节序的系统,中央处理器可能会以不同的顺序加载数据,从而导致访问该文件的应用程序无法正确解释其数据。

重新思考CPU:CISC与RISC

大约在1980年,一种被称为精简指令集计算机(RISC)的新概念从IBM托马斯·J·沃森研究中心、加州大学伯克利分校和斯坦福大学的实验室中诞生。这些研究项目的成果最终分别发展成为广受欢迎的POWER(采用增强型RISC的性能优化)、SPARC(可扩展处理器架构)和MIPS(无互锁流水线级的微处理器)架构,它们体现了一种与当时主流技术截然不同的中央处理器设计思路。术语复杂指令集计算机(CISC)则是后来为指代此前的架构而提出的。在过去三十年中,RISC与 CISC架构之间的竞争一直是计算机产业的决定性特征之一。

到20世纪70年代中期,用于小型计算机和大型机的高性能中央处理器的设计已集中于两个关键目标:提高代码密度,以及弥合与当时高级编程语言之间的语义鸿沟。这两个目标都促使设计者将越来越多的功能集成到单条机器指令中。回顾计算技术发展初期的指令集,可以看到一些奇特而怪异的例子:某第一代CPU具有一条指令,能够触发对早期视频显示器进行拍摄的摄像头;另一款CPU则具有一条指令,可升起所连接系统打印机的保护盖。请注意,这些并非库例程或实用程序,而是真正内置于中央处理器中的机器指令。

对更高代码密度的需求源于内存的高成本和相对较低的速度。正如第3章所述,在计算机发展的大部分历史中,系统内存都极其昂贵。当内存价格高昂时,内存系统必然很小。DEC PDP‐8小型计算机的总物理地址空间仅为4096字节。在设计PDP‐8的时代,这已经是一般购买者所能负担的全部内存。更大的程序虽然可以运行,但只有在操作系统开始实现虚拟内存后才成为可能(参见第3章)。

在这种情况下,显然保持程序物理长度较短具有优势。复杂且语义丰富的指令有助于减少指令数量:一条占用内存2字节的“摄像头拍照”机器指令可以替代可能占用50或 100字节内存的“摄像头拍照”子程序。到了20世纪70年代中后期,大容量DRAM芯片的出现降低了不惜一切代价追求代码密度的紧迫性。(顺便提一下,正是低廉的内存成本,与低廉的中央处理器成本同样重要,才使得第一代个人计算机成为可能:如果每千字节内存的价格高达5000美元,那么一个100美元的中央处理器芯片对您几乎毫无帮助。)

术语“语义鸿沟”指的是高级语言中可表达的行为(如嵌套循环、函数调用、多维数组索引)与底层硬件提供的行为(条件和无条件的非结构化分支、从内存地址加载和存储的能力)之间的差异。微码技术使得设计者能够创建直接在机器语言级别实现高级特征的指令,从而缩小了这一鸿沟。编译器或谨慎的底层程序员可以通过使用这些指令获得显著的性能提升,但在实践中,大多数编译器出于简洁性和在不同架构之间的可移植性考虑,选择忽略这些指令。人们观察到一个粗略的80/20法则,即20%的指令被使用了80%的时间,而许多指令根本未被使用。引人注目的是,编译器所使用的“精简指令集”与中央处理器内部提供的微指令非常相似。

最早的实验性精简指令集计算机通过仅提供由非常简单的指令组成的小型指令集来利用这一见解;它们可以被视为中央处理器只是将其微指令暴露给外部世界。虽然实现一个程序需要更多的RISC指令,但与复杂指令集架构相比,程序性能却非常出色;因为RISC指令执行速度非常快,其简单的执行方式使得应用流水线等技术变得更加容易,而且编译器本来就没有使用那些更复杂的指令。

RISC CPU 的一个显著特征是,其所有或几乎所有指令都通过硬连线逻辑实现。事实上,如今微码甚至已被排除在仅存的主要 CISC 架构——英特尔 x86 的内部结构之外。自 2000 年推出 Netburst 微架构以来,英特尔处理器在内部以类似 RISC 的微操作运行,传统的 CISC 指令在流水线最前端被转换为独立发出的微操作。

与此同时,精简指令集计算机处理器通过增加指令集特征来追求性能的逐步提升,具有讽刺意味的是,也为了提高代码密度,以至于曾经清晰可辨的精简指令集计算机与复杂指令集计算机之间的区别已变得非常模糊。当初简化指令集的许多动机源于希望将有限的晶体管预算重新用于新的性能特征,例如缓存和大幅扩展的寄存器组。随着1990年代晶体管预算的激增,指令集的扩展再次成为可能。如今,许多精简指令集架构(包括 ARM)所拥有的指令数量与其复杂指令集计算机 counterparts 大致相同。

RISC的传承

尽管RISC与CISC之间的界限已变得模糊,但仍然可以识别出RISC运动为CPU架构带来的一些关键特性:
- 扩展寄存器文件
- 加载/存储架构
- 正交机器指令
- 指令和数据的分离缓存

还有第五个精简指令集计算机特征,并不是每个人都能理解:精简指令集计算机是一次全新的开始。凭借近40年的经验,计算机科学家从头开始重新构想中央处理器架构。基于20年前技术限制的假设被摒弃。对支持“传统”代码的需求也不复存在。英特尔当前的x86架构仍然反映了为方便将1974年8080芯片的程序转换到1980年 8086芯片上而做出的设计决策。而精简指令集架构则无需支持此类传统。

让我们更仔细地看看这些特性。

扩展寄存器文件

作为一个整体,中央处理器的寄存器被称为其register file或register set。机器寄存器在晶体管预算方面是“昂贵的”。早期中央处理器的寄存器非常少,且容量很小。8080有七个8位寄存器可用于常规编程。流行的摩托罗拉6800和MOS科技6502各自仅有三个。相比之下,最初的ARM CPU拥有13个32位通用寄存器,而后来的 POWER 精简指令集计算机处理器则拥有32个。

寄存器是整个计算机中最快的数据存储位置。从内存读取数据所需的时间远大于在寄存器中处理数据的时间。如果有足够的寄存器来保存操作数和中间结果,程序就可以尽可能地避免访问内存(从而始终停留在速度更快的中央处理器内部)。这通过避免往返内存访问(或至少减少对缓存的访问)来提高性能,并有助于现代乱序超标量处理器识别指令级并行的机会。

加载/存储架构

在大多数复杂指令集架构中,机器指令可以直接对存储在系统内存中的数据进行操作。这是因为复杂指令集架构较为陈旧,且通常存在寄存器匮乏的问题。一条典型的复杂指令集加法指令可以将一个寄存器的内容或一个立即数与内存中的一个数据字相加:

ADD [内存地址], 8

此指令将立即值8加到第一个操作数所指定地址的内存地址中。此类指令执行速度较慢,因为简单的加法需要两次内存访问:一次从内存中读取原始值,另一次将新值写回内存。在实际程序中,此类加法通常是更长操作序列的一部分。如果这些计算都能在寄存器内完成,则对内存的访问会大大减少。然而,当所有寄存器都处于繁忙状态时,便没有其他选择。

通过访问更大的寄存器文件,精简指令集架构通常会从大多数指令中移除内存访问功能,使这些指令仅对寄存器进行操作。访问内存则成为一小部分专用机器指令的特殊功能,这些指令除此之外不执行其他任务。

以这种方式设计CPU会形成一种加载/存储架构。通过专用的加载指令从内存中将值加载到寄存器中,在寄存器内部进行运算,然后通过专用的存储指令将结果从寄存器写回内存。其目标(与现代计算机架构中的几乎所有设计一样)是尽可能减少对内存的访问,并简化流水线的内部工作。

正交机器指令

大多数CISC指令具有深厚的历史渊源。随着计算机架构在20世纪50年代和20世纪60年代的发展,为了应对新的需求,新的指令被不断添加到指令集中。这往往导致CISC指令集成为由多种长度的多字节指令拼凑而成的大杂烩。这些指令集并非整体设计而成,而是“逐渐积累”形成的。

这种临时设计的指令集存在的第二个问题是,许多指令在访问内存或寄存器的方式上属于特殊情况。例如,早期中央处理器有一个称为累加器的寄存器,用于存放算术和逻辑指令操作的数值。(该名称源于一些非常早期的计算机和机电制表机将中间结果累积在一个指定寄存器中的事实。)许多早期指令具有特殊形式,将累加器视为寄存器中的特例。

特殊情况使得解码和执行指令比原本更加复杂且耗时。因此,当计算机科学家从头开始设计新的精简指令集架构指令集合时,他们取消了特殊情形,并使所有指令长度保持一致。对于32位RISC架构(包括原始树莓派的ARM11 CPU),该长度几乎总是一个32位字。

一种指令集设计,使得所有指令长度相同且CPU资源无需特殊处理,被称为正交的。机器指令的内部结构也进行了标准化,以简化指令解码,我们将在后面进一步解释。

分离缓存用于指令和数据

如第2章所述,最早的计算机(例如哈佛大学1944年的Mark I机器)将机器指令和数据存储在完全独立的内存系统中。约翰·冯·诺依曼指出,机器指令在物理上与数据并无不同,两者应存放在同一个内存系统中。

创建早期RISC CPU的计算机科学家在一定程度上背离了冯·诺依曼原理。他们证明,尽管代码和数据应存储在同一个内存系统中,但采用分离的指令缓存和数据缓存能带来性能优势。StrongARM微架构是首个采用分离代码和数据缓存的ARM指令集架构实现。缓存在CPU性能中的重要作用体现在:在StrongARM硅芯片上的250万个晶体管中,设计者选择将60%的晶体管用于这两个缓存。ARM11微架构也采用了这种“改进型哈佛架构”,并具有分离缓存。

性能提升的原因在于局部性的概念,如第3章所述。机器指令通常存储在内存中与程序数据分开的区域。更重要的是,内存中的指令通常按顺序访问,因为程序被执行。数据被组织成存储字的块,可根据程序需求以任意顺序访问。数据访问可能并非真正随机,但很少是顺序的。独立的代码和数据缓存允许使用不同的替换策略以及可能不同的缓存行大小(参见第3章),以适应每种缓存的访问模式。

当然,并非所有精简指令集架构都相同。在精简指令集计算机35年的发展历史中,人们尝试了许多不同的设计。精简指令集设计原则的成功之处在于,大多数现代复杂指令集架构都融入了许多精简指令集的特性,包括主导地位的复杂指令集架构——英特尔的x86。

本章其余部分将重点介绍一种特定的精简指令集CPU系列:ARM控股有限公司的 ARM处理器,特别是ARM11处理器以及后续的ARM Cortex处理器。

小橡子长成ARM

1981年初,英国广播公司(BBC)启动了一个项目,旨在提升观众尤其是年轻人的计算机技能。该计算机素养项目需要一台坚固且价格合理的大众市场计算机作为课程的基础。该项目发布了技术规格并邀请厂商投标。唯一符合其规格的设计是来自剑桥的Acorn计算机公司的Proton,该公司与树莓派基金会一样位于剑桥。

Proton基于与流行的Apple II机器相同的6502微处理器。在被BBC采用后,Proton被称为BBC微型计算机,并售出了超过150万台。

一旦IBM PC使个人计算机在商业应用中得到认可,Acorn便决定开发一款高端机型以进军办公市场。它评估了当时所有主流的微处理器,包括8086和68000,但因各种原因发现它们均不适用。1983年,Acorn启动了一项雄心勃勃的项目,旨在设计自己的微处理器,用于其高端系统。

由Acorn工程师索菲·威尔逊和史蒂夫·弗伯领导的团队借鉴了源自伯克利RISC项目的科研成果。Acorn精简指令集机器(ARM)中央处理器的首批硅芯片于1985年年中返回。ARM1是一个从未商业化的原型。量产芯片于1986年出现,即ARM2。ARM2微处理器最初作为协处理器用于基于6502的BBC微型计算机中,以提升机器性能,特别是在图形和计算机辅助设计(CAD)等领域。

第一台完整的基于ARM的微型计算机于1987年作为Acorn Archimedes发布。Archimedes为Acorn带来了新事物:Arthur,一种具有完整图形用户界面的操作系统。Arthur后来发展成为RISC OS,该操作系统至今仍然存在。

RISC OS 可免费下载用于树莓派。您可以在 https://www.riscosopen.org/wiki/documentation/show/Welcome to RISC OS Pi 获取有关 RISC OS 的更多信息(以及树莓派的版本)。

ARM CPU的开发于1990年剥离至一家独立公司,当时ARM缩写变更为先进RISC机器。先进RISC机器公司于1998年更名为ARM控股公司。

微架构、核心和系列

ARM对其产品的命名有时可能会令人困惑。ARM处理器的指令集架构(ISA)具有版本号,而ARM微架构也有独立的版本号。微架构指的是中央处理器设计者在硅中实现指令集架构的方式。可以这样理解:ISA定义了CPU行为,而微架构定义了其结构。

ARM处理器按系列分组,每个系列都有其对应的微架构版本号。最早的ARM指令集架构版本是ARMv1,仅用于原型ARM1处理器。ARMv2指令集架构实现在ARM2和ARM3系列的中央处理器中。ARMv3实现在ARM6和ARM7系列中,依此类推。最初的树莓派所使用的中央处理器属于ARM11系列,该系列实现了ARMv6指令集。同一ARM系列内的处理器通常仅在一些细节上有所差异,这些差异更多体现的是设计侧重,而非显著的架构区别。ARM11微架构适用于ARM11系列中的全部四个核心。

你经常会听到ARM CPU被称为“核心”。在计算机产业中,“core”并不是一个精确的技术术语。大多数情况下,它表示可能存在于包含多个核心的单芯片设计中的任何大型独立组件。在ARM领域中,“核心”更具体地指可被集成到定制设备中的中央处理器,该设备还包括非中央处理器逻辑电路,如USB和网络端口、图形处理器、大容量存储控制器、定时器、总线控制器等。这种设备被称为system-on-a-chip(片上系统)。

销售许可证而非芯片

一旦你理解了ARM控股公司和英特尔所采用的商业模式之间的根本差异,ARM对“核心”的特定定义就会开始变得更容易理解。

英特尔设计并制造成品芯片,每颗芯片都封装在独立的塑料或陶瓷集成电路封装中,可直接插入或焊接到计算机电路板上。相比之下,ARM控股公司则纯粹是一家设计公司。其工程师设计CPU核心及其他计算机逻辑电路,然后将这些设计授权给其他公司。获得ARM设计授权的公司可以对设计进行定制,或将其与内部开发的逻辑电路集成,从而创建完整的SoC设计。随后,他们将设计交给名为芯片代工厂的公司,由其代为制造集成电路。

只要计算机产业仍由成熟且大多相同的笔记本电脑和台式机设计主导,英特尔的商业模式就占据主导地位。然而,在智能手机和平板电脑进入大众市场后,定制不仅对于产品差异化至关重要,而且对于产品的演进也变得关键。ARM驱动设备的创新深入到了CPU硅片层面。大多数授权方使用现成且经过认证的ARM核心,但 ARM也已将其ISA授权给一些架构授权方,这些授权方随后创建了代表非ARM微架构的自定义核心。最早的例子是StrongARM核心,该核心由数字设备公司于1990年代设计,后来出售给英特尔成为XScale。StrongARM/XScale在一种新颖的微架构中实现了ARMv4 ISA;它是ARM系列中首个采用独立指令和数据缓存的CPU。更近期的架构授权方包括苹果(其Swift核心)和高通(其Scorpion及后续的 Krait核心)。

树莓派计算机均采用由博通设计的系统级芯片。第一代开发板包含一个ARM11核心。第二代和第三代开发板各自包含四个Cortex系列核心。接下来,我们将转向对 ARM11微架构的更详细描述。在本章的后面部分,我们将探讨树莓派的片上系统设备以及系统级芯片的设计方式。

ARM11

ARM11微架构于2002年发布,是首个且迄今为止唯一实现ARMv6 ISA的ARM系列。它是一种32位微架构,意味着所有机器指令均为32位宽,并以32位字为单位访问内存。某些ARM机器指令设计用于操作更小的操作数,共有两种类型:16位半字和8位字节。

ARM指令集

ARMv6指令集架构包含三个独立的指令集:ARM、Jazelle和Thumb。其中,ARM指令集使用最为频繁。

本章中(以及本书其他章节,包括第5章中的一个完整程序)偶尔会看到一些 ARM机器指令,因此最好快速了解一下机器指令的构成方式。让我们来看几个例子。

我们谨慎地使用“构建”一词,因为ARM机器指令允许各种选项,以便根据需要以不同方式工作。

最容易理解的机器指令是那些对数据执行算术运算的指令。请记住,我们之前讨论过,精简指令集计算机的机器指令不直接访问内存。所有对数据的操作都是使用存储在寄存器中的数据完成的。考虑一下ADD指令,它将两个寄存器的内容相加,并将和放入第三个寄存器中。ADD指令的一般汇编语言形式如下所示:

ADD{<条件码>} {S} <Rd>,<Rn>,<Rm>

指令在大多数ARM指令参考中均以这种方式进行总结。该表示法的含义如下:
- 大括号({})内的内容为可选项,不在大括号内的内容为必选项。
- 尖括号内的内容(<>)是符号或值的占位符。
- Rd 表示目标寄存器。当一条指令具有目标寄存器操作数时,目的操作数位于助记符之后的第一个位置。Rn 和 Rm 用于命名源寄存器操作数。m 和 n 并不代表任何特定含义。

几乎所有的ARM指令都可以条件执行。(我们将在本章后面详细讨论这一点。)可选的<条件码>指定了15种条件之一,必须满足该条件才能执行指令的操作。如果条件码未满足,指令仍会通过流水线,但不会执行其他任何操作。如果没有指定条件码,则默认为“始终”,表示无条件执行。

可选的S后缀指示ADD指令根据加法结果修改条件标志;这些标志随后控制任何后续有条件执行的指令。如果没有S后缀,机器指令在执行操作时不会更改标志位的值。这意味着一系列指令可以根据最初设置标志位的操作,有条件地执行其工作。

以下指令用于将寄存器1(R1)的内容与寄存器2(R2)的内容相加,并将和存入寄存器 5(R5):

ADD R5, R1, R2

要构建一条仅在Zero标志位被设置时才执行的指令,需将条件码EQ添加到助记符中:

ADDEQ R5, R1, R2

减法的工作方式与此非常相似。一条将R3从R4中减去并将差值存入R2的指令会如下所示,假设程序员希望该减法操作能够设置标志位:

SUBS R2, R4, R3

并非所有指令都需要三个操作数。MOV 指令将一个寄存器中存储的值复制到另一个寄存器,或将一个立即值放入寄存器中:

MOV R5, R3
MOV R5, #42

第一条指令将R3中的内容复制到R5中。第二条指令将立即值42存储到R5中。

尽管《ARM架构参考手册》已不再普遍提供,但它对于了解多种ARM指令集仍具有很高的参考价值。(有时可以通过对标题进行谷歌搜索找到可下载的资源。)编写简短的汇编语言程序,并在调试器中检查其执行过程,是观察各种指令作用的良好方法。Raspbian操作系统附带的GNU编译器集合包含一个非常优秀的汇编器。第5章解释了如何汇编和运行简短的汇编语言测试程序。

Jazelle

Jazelle指令集允许ARM11核心直接执行Java字节码,而无需软件解释。(第5章介绍了Java和Python等字节码语言。)ARM控股公司于2011年弃用了Jazelle,这意味着该公司将不再进一步发展该技术,并建议在新项目中不要使用它。

计算机制造商有时会在认为某个功能或产品线已达到其使用寿命终点时将其标记为已弃用。这并不意味着他们会禁用该功能,而是强烈建议不再在将来使用。许多制造商还指出,已弃用的产品或功能可能会在将来的某个时间被撤销,或者对其的支持会以各种方式被取消。由于这些原因,在新设计中不应使用已弃用的功能和产品。

Thumb

Thumb指令集是32位ARM指令集的16位实现。Thumb指令的宽度为16位,而不是32位。这实现了更高的代码密度,意味着在给定的内存空间中可以存储更多的指令(从而实现更多功能)。一些低端设备具有有限内存,并且通过16位系统总线每次以16位的方式访问该内存。Thumb指令的设计使其能更高效地利用此类总线。

Thumb指令仍然在寄存器中处理32位数据。并非所有寄存器都对Thumb指令完全可用,某些其他硬件资源也只能以有限方式使用。

Thumb指令集之所以引人关注,还有另一个原因:在从内存或缓存中获取Thumb指令后,它们会通过中央处理器内部的专用逻辑电路扩展为普通的ARMv6指令。一旦进入指令流水线,这些指令就不再是以Thumb指令的形式存在。因此,Thumb指令可视为一种缩写形式,使得在给定的内存空间内可以容纳更多的指令。Thumb指令集通常用于编程嵌入式系统,这类设备通过集成微处理器和软件来实现其功能,但本身并非通用计算机。然而,这一界限并不十分明确:例如树莓派虽然具备足够的内存和中央处理器性能,可用作传统桌面计算机,但也常被用于嵌入式系统。

当ARM11核心执行Thumb指令时,称其处于Thumb状态。类似地,核心在执行Jazelle指令时处于Jazelle状态。在几乎所有情况下,树莓派都在ARM状态中运行,使用完整的32位ARM指令集。

不要将处理器状态与处理器模式混淆。请继续阅读。

处理器模式

早期的桌面操作系统很少或根本没有防止应用程序行为异常的机制。事实上,CP/M‐80 系统的内存非常有限,以至于 CP/M‐80 在启动应用程序后基本上会从内存中自行卸载,并在应用程序终止后重新加载自身。PC‐DOS 虽然保留在内存中,但 Windows 在 1993 年首次发布 Windows NT 之前,只是运行在 PC‐DOS 之上的用户界面,而不是一个操作系统。因此,CP/M‐80 和 PC‐DOS 更准确地说应被视为系统监视器,而非操作系统。

监控器 是一种系统软件,用于加载和运行应用程序,但在管理系统资源方面功能有限。

问题的一部分是内存不足,但更大的问题是当时的CPU芯片无法保护系统软件免受应用程序的影响。1985年,英特尔的386中央处理器是首批提供实用保护模式的英特尔芯片,该模式可为操作系统内核提供对系统资源的特权访问,而应用程序(运行在实模式或用户模式下)则无法访问这些资源,这是在英特尔处理器上实现真正操作系统的前提条件。所有用于通用计算机的现代CPU都包含用于管理系统资源的逻辑电路,并防止应用程序干扰操作系统和其他应用程序。

ARM11处理器提供了多种不同的模式,以支持操作系统对用户应用程序和计算机硬件的管理。这些模式在表4‐1中进行了总结。除用户模式外,所有其他模式都被视为特权模式,意味着它们可以完全访问系统资源。管理员模式专供操作系统内核及与操作系统相关的受保护代码使用。系统模式基本上是具有全部权限并可访问所有硬件的用户模式。它很少被使用,仅在低端嵌入式工作中有所应用;目前被认为已过时。

表4‐1 ARM11 处理器模式

Mode 缩写 模式位 描述
User usr 10000 用于用户应用程序执行
监督模式 svc 10011 用于操作系统内核
系统 Sys 11111 现已过时
安全监控器 mon 10110 用于 TrustZone 应用程序
FIQ fiq
内容概要:本文是《目标检测入门指南》系列的第二部分,重点介绍用于图像分类的经典卷积神经网络(CNN)架构及其在目标检测中的基础作用。文章详细讲解了卷积操作的基本原理,并以AlexNet、VGG和ResNet为例,阐述了不同CNN模型的结构特点创新点,如深层网络设计、小滤波器堆叠和残差连接机制。同时介绍了目标检测常用的评估指标mAP(平均精度均值),解释了其计算方式和意义。此外,文章还回顾了传统的可变形部件模型(DPM),分析其基于根滤波器、部件滤波器和空间形变代价的检测机制,并指出DPM可通过展开推理过程转化为等效的CNN结构。最后,介绍了Overfeat模型,作为首个将分类、定位检测统一于CNN框架的先驱工作,展示了如何通过滑动窗口进行多尺度分类并结合回归器预测边界框。; 适合人群:具备一定计算机视觉和深度学习基础,从事或学习图像识别、目标检测相关方向的研发人员学生;适合希望理解经典CNN模型演进及目标检测早期发展脉络的技术爱好者。; 使用场景及目标:①理解CNN在图像分类中的核心架构演变及其对后续目标检测模型的影响;②掌握mAP等关键评估指标的含义计算方法;③了解DPMOverfeat的设计思想,为深入学习R-CNN系列等现代检测器打下理论基础。; 阅读建议:此资源以综述形式串联多个经典模型,建议结合原文图表参考文献进行延伸阅读,并通过复现典型模型结构加深对卷积、池化、残差连接等操作的理解,从而建立从传统方法到深度学习的完整认知链条。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值