03-原理篇:处理器(26-29)

340 阅读20分钟

26 | Superscalar和VLIW:如何让CPU 的吞吐率超过1?

程序的 CPU 执行时间 = 指令数 × CPI × Clock Cycle Time

CPI 的倒数,又叫作 IPC(Instruction Per Clock),也就是一个时 钟周期里面能够执行的指令数,代表了 CPU 的吞吐率。

这个指标,放在我们前面几节反复优化流水线架构的 CPU 里,能达到多少呢? 答案是,最佳情况下,IPC 也只能到 1。因为无论做了哪些 流水线层面的优化,即使做到了指令执行层面的乱序执行, CPU 仍然只能在一个时钟周期里面,取一条指令。

无论指令后续能优化得多好,一个时钟周期也只能 执行完这样一条指令,CPI 只能是 1。但是,我们现在用的 Intel CPU 或者 ARM 的 CPU,一般的 CPI 都能做到 2 以 上,这是怎么做到的呢?

多发射与超标量:同一实践执行的两条指令

虽然浮点数计算已经变成 CPU 里的一部分,但并不是所有计算功能都在一个 ALU 里面,真实的情况是,我们会有多个 ALU。
所以指令的执行阶段,是由很多个功能单元(FU)并行 (Parallel)进行的。
不过,在指令乱序执行的过程中,我们的取指令(IF)和指令译码(ID)部分并不是并行进行的。

既然指令的执行层面可以并行进行,为什么取指令和指令译码不行呢? 其实只要我们把取指令和指令译码,也一样通过增加硬件的 方式,并行进行就好了。我们可以一次性从内存里面取出多 条指令,然后分发给多个并行的指令译码器,进行译码,然 后对应交给不同的功能单元去处理。这样,我们在一个时钟 周期里,能够完成的指令就不止一条了。IPC 也就能做到大于 1 了。

image.png

这种 CPU 设计,我们叫作多发射(Mulitple Issue)和超标量(Superscalar)。

多发射:同一个时间,可能会同时把多条指令发射(Issue)到不同的译码器或者后续处理的流水线中去。

在超标量的 CPU 里面,有很多条并行的流水线,而不是只有一条流水线。“超标量“这个词是说,本来我们在一个时钟周期里面,只能执行一个标量(Scalar)的运算。在多发射的情况下,我们就能够超越这个限制,同时进行多次计 算。

每一个功能单元的流水线的长度是不同的。事实上,不同的功能单元的流水线长度本来就不一样。我们平时所说的 14 级流水线,指的通常是进行整数计算指令的流水线长度。如果是浮点数运算,实际的流水线长度则会更长一些。

Intel 的失败之作:安腾的超长指令字设计

在乱序执行和超标量的体系里面,我们的 CPU 要解决依赖冲突的问题。这也就是前面几讲我们讲的冒险问题。

CPU 需要在指令执行之前,去判断指令之间是否有依赖关系。如果有对应的依赖关系,指令就不能分发到执行阶段。 因为这样,上面我们所说的超标量 CPU 的多发射功能,又被称为动态多发射处理器。这些对于依赖关系的检测,都会 使得我们的 CPU 电路变得更加复杂。

超长指令字设计(Very Long Instruction Word,VLIW):这个设计呢,不仅想让编译 器来优化指令数,还想直接通过编译器,来优化 CPI。
显式并发指令运算(Explicitly Parallel Instruction Computer),这个名字的缩写EPIC,正好是“史诗”的意思。

在乱序执行和超标量的 CPU 架构里,指令的前后依赖关系,是由 CPU 内部的硬件电路来检测的。而到了超长指令 字的架构里面,这个工作交给了编译器这个软件。

image.png

可以让编译器把没有依赖关系 的代码位置进行交换。然后,再把多条连续的指令打包成一个指令包。安腾的 CPU 就是把 3 条指令变成一个指令包。

image.png CPU 在运行的时候,不再是取一条指令,而是取出一个指 令包。然后,译码解析整个指令包,解析出 3 条指令直接并 行运行。可以看到,使用超长指令字架构的 CPU,同样是 采用流水线架构的。也就是说,一组(Group)指令,仍然 要经历多个时钟周期。同样的,下一组指令并不是等上一组 指令执行完成之后再执行,而是在上一组指令的指令译码阶 段,就开始取指令了。

流水线停顿这件事情在超长指令字里 面,很多时候也是由编译器来做的。除了停下整个处理器流 水线,超长指令字的 CPU 不能在某个时钟周期停顿一下, 等待前面依赖的操作执行完成。编译器需要在适当的位置插 入 NOP 操作,直接在编译出来的机器码里面,就把流水线 停顿这个事情在软件层面就安排妥当。

安腾失败的原因有很多,其中有一个重要的原因就是“向前兼容”。
一方面,安腾处理器的指令集和 x86 是不同的。这就意味 着,原来 x86 上的所有程序是没有办法在安腾上运行的,而需要通过编译器重新编译才行。
另一方面,安腾处理器的 VLIW 架构决定了,如果安腾需要 提升并行度,就需要增加一个指令包里包含的指令数量,比方说从 3 个变成 6 个。一旦这么做了,虽然同样是 VLIW 架构,同样指令集的安腾 CPU,程序也需要重新编译。因为原来编译器判断的依赖关系是在 3 个指令以及由 3 个指 令组成的指令包之间,现在要变成 6 个指令和 6 个指令组 成的指令包。编译器需要重新编译,交换指令顺序以及 NOP 操作,才能满足条件。甚至,我们需要重新来写编译 器,才能让程序在新的 CPU 上跑起来。 于是,安腾就变成了一个既不容易向前兼容,又不容易向后兼容的 CPU。那么,它的失败也就不足为奇了。

27 | SIMD:如何加速矩阵乘法?

超线程:Intel 多卖给你的那一倍 CPU

超长的流水线,使得之前我们讲的很多解决“冒险”、提升并发的方案都用不上。
因为这些解决“冒险”、提升并发的方案,本质上都是一种指令级并行(Instruction-level parallelism,简称 IPL)的技术方案。换句话说就是,CPU 想要在同一个时间,去并行地执行两条指令。而这两条指令呢,原本在我们的代码里,是有先后顺序的。无论是我们在流水线里面讲到的流水线架构、分支预测以及乱序执行,还是我们在上一讲说的超标量和超长指令字,都是想要通过同一时间执行两条指令,来提升 CPU 的吞吐率。
然而在 Pentium 4 这个 CPU 上,这些方法都可能因为流水线太深,而起不到效果。我之前讲过,更深的流水线意味着同时在流水线里面的指令就多,相互的依赖关系就多。于是, 很多时候我们不得不把流水线停顿下来,插入很多 NOP 操作,来解决这些依赖带来的“冒险”问题。

超线程技术:既然 CPU 同时运行那些在代码层面有前后依赖关系的指令,会遇到各种冒险问题,我们不如去找一些和这些指令完全独立,没有依赖关系的指令来运行好了。那么,这样的指令哪里来呢?自然同时运行在另外一个程序里了。

无论是多个CPU核心运行不同的程序,还是在单个CPU核心里面切换运行不同线程的任务,在同一时间点上,一个物理的 CPU 核心只会运行一个线程的指令,所以其实我们并没有真正地做到指令的并行运行。

image.png

超线程可不是这样。超线程的 CPU,其实是把一个物理层面 CPU 核心,“伪装”成两个逻辑层面的 CPU 核心。这个 CPU,会在硬件层面增加很多电路,使得我们可以在一个 CPU 核心内部,维护两个不同线程的指令的状态信息。

比如,在一个物理 CPU 核心内部,会有双份的 PC 寄存器、指令寄存器乃至条件码寄存器。这样,这个 CPU 核心就可以维护两条并行的指令的状态。在外面看起来,似乎有两个逻辑层面的 CPU 在同时运行。所以,超线程技术一般也被叫作同时多线程(Simultaneous Multi-Threading,简称 SMT)技术。

不过,在 CPU 的其他功能组件上,Intel 可不会提供双份。无论是指令译码器还是 ALU, 一个 CPU 核心仍然只有一份。因为超线程并不是真的去同时运行两个指令,那就真的变成 物理多核了。超线程的目的,是在一个线程 A 的指令,在流水线里停顿的时候,让另外一 个线程去执行指令。因为这个时候,CPU 的译码器和 ALU 就空出来了,那么另外一个线程 B,就可以拿来干自己需要的事情。这个线程 B 可没有对于线程 A 里面指令的关联和依赖。

这样,CPU 通过很小的代价,就能实现“同时”运行多个线程的效果。通常我们只要在 CPU 核心的添加 10% 左右的逻辑功能,增加可以忽略不计的晶体管数量,就能做到这一点。 不过,并没有增加真的功能单元。所以超线程只在特定的应用场景下效果 比较好。一般是在那些各个线程“等待”时间比较长的应用场景下。比如,我们需要应对很多请求的数据库应用,就很适合使用超线程。各个指令都要等待访问内存数据,但是并不需要做太多计算。

SIMD:如何加速矩阵乘法?

SIMD,中文叫作单指令多数据流(Single Instruction Multiple Data):向量化 SISD,也就是单指令单数据:使用循环来一步一步计算 如果你手头的是一个多核 CPU 呢,那么 它同时处理多个指令的方式可以叫作MIMD,也就是多指令多数据(Multiple Instruction Multiple Data)。

为什么 SIMD 指令能快那么多呢:
这是因为,SIMD 在获取数据和执行指令的时候,都做到了并行。一方面,在从内存里面读取数据的时候,SIMD 是一次性读取多个数据。在数据读取到了之后,在指令的执行层面,SIMD 也是可以并行进行的。

28 | 异常和中断:程序出错了怎么办?

计算机 究竟是如何处理异常的。

异常:硬件、系统和应用的组合拳

尽管,这里我把这些硬件和系统相关的异常,叫作“硬件异常”。但是,实际上,这些异常,既有来自硬件的,也有来自软件层面的。
比如,我们在硬件层面,当加法器进行两个数相加的时候,会遇到算术溢出;或者,你在玩 游戏的时候,按下键盘发送了一个信号给到 CPU,CPU 要去执行一个现有流程之外的指 令,这也是一个“异常”。 同样,来自软件层面的,比如我们的程序进行系统调用,发起一个读文件的请求。这样应用 程序向系统调用发起请求的情况,一样是通过“异常”来实现的。

关于异常,最有意思的一点就是,它其实是一个硬件和软件组合到一起的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处理,其实是由软件来完成的。

计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number)。有些教 科书会把异常代码叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检 测到了一个特殊的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。 或者,正在执行的指令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号 呢,在组成原理里面,我们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。

这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又是另一个软件和硬件共同组合来处理异常的过程。

拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表 (Exception Table)。也有地方,把这个表叫作中断向量表(Interrupt Vector Table),好和上面的中断向量对应起来。存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。

CPU 在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然 后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。

image.png

异常的分类:中断、陷阱、故障和中止

中断(Interrupt):顾名思义,自然就是程序在执行到一半的时候,被打断了。这个打断执行的信号,来自于 CPU 外部的 I/O 设备。你在键盘上按下一个按键,就会对应触发一个相应的信号到达 CPU 里面。CPU 里面某个开关的值发生了变化,也就触发了一个中断类型的异常。

陷阱(Trap):陷阱,其实是我们程序员“故意“主动触发的异常。就好像你在程序里面打了一个断点,这个断点就是设下的一个"陷阱"。当程序的指令执行到这个位置的时候,就掉到了这个陷阱当中。然后,对应的异常处理程序就会来处理这个"陷阱"当中 的猎物。

故障(Fault):它和陷阱的区别在于,陷阱是我们开发程序的时候刻意触发的异常,而故障通常不是。比如,我们在程序执行的过程中,进行加法计算发生了溢出,其实就是故障类型的异常。这个异常不是我们在开发的时候计划内的,也一样需要有对应的异常处理程序去处理。
故障和陷阱、中断的一个重要区别是,故障在异常程序处理完成之后,仍然回来处理当前的 指令,而不是去执行程序中的下一条指令。因为当前的指令因为故障的原因并没有成功执行 完成。

中止(Abort):与其说这是一种异常类型,不如说这是故障的一种特殊情 况。当 CPU 遇到了故障,但是恢复不过来的时候,程序就不得不中止了。

image.png

异常的处理:上下文切换

切换到异常处理程序的时候,其实就好像是去调用一个异常处理函数。指令的控制权被 切换到了另外一个"函数"里面,所以我们自然要把当前正在执行的指令去压栈。这样,我们 才能在异常处理程序执行完成之后,重新回到当前的指令继续往下执行。

不过,切换到异常处理程序,比起函数调用,还是要更复杂一些。

  1. 因为异常情况往往发生在程序正常执行的预期之外,比如中断、故障发生的时候。所以,除了本来程序压栈要做的事情之外,我们还需要把 CPU 内当前运行程序用到的所有寄存器,都放到栈里面。最典型的就是条件码寄存器里面的内容。

  2. 像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里。

  3. 像故障这样的异常,在异常处理程序执行完成之后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令。因为当前指令因为故障没有正常执行成功,必须重新去执行一次。

所以,对于异常这样的处理流程,不像是顺序执行的指令间的函数调用关系。而是更像两个不同的独立进程之间在 CPU 层面的切换,所以这个过程我们称之为上下文切换(Context Switch)。 (函数调用是预期的,所以寄存器不用管(里面是正常内容),异常类似于运行新的程序,所以寄存器必须保存)

问题1:很多教科书和网上的文章,会把中断分成软中断和硬中断。你能用自己的话说一说,什么是软中断,什么是硬中断吗?它们和我们今天说的中断、陷阱、故障以及中止又有什么关系呢?

在计算机操作系统中,中断是程序执行暂停的一种情况,当CPU接收到来自外部设备或软件模块的请求时发生。软中断和硬中断都可以触发这种响应,但它们之间有一些关键的区别。

软中断,也称为软件中断,是内核级中断,由CPU直接进入内核的软中断处理程序(也称为中断服务例程或ISR)。软中断通常用于需要定期或异步执行的任务,例如网络数据包处理或磁盘I/O。

另一方面,硬中断是由外部设备触发的,例如键盘或鼠标发送信号到CPU。当CPU接收到信号时,它会中断当前程序并跳转到适当的中断处理程序以处理请求。硬中断通常于需要立即处理的时间关键任务,例如实时数据采集或设备控制。

  • 中断是由外部设备或件模块触发的,用于暂停程序执行并处理请求。
  • 陷阱是由程序中的指令触发的,用于执行特定的系统调用或软件中断。
  • 故障是由程序中的错误或异常情况触发的,例如访问无效的内地址或除以零。
  • 中止是由操作系统或其他程序强制终止当前程序的执行。

29 | CISC和RISC:为什么手机芯片都是 ARM?

CPU 的指令集里的机器码是固定长度还是可变长度,也就是复杂指令集(Complex Instruction Set Computing, 简称 CISC)和精简指令集(Reduced Instruction Set Computing,简称 RISC)这两种风格的指令集一个最重要 的差别。

CISC VS RISC:历史的车轮不总是向前的

早期CPU 指令集的设计,需要仔细考虑硬件限制。为了性能考虑,很多功能都直接通过硬件电路来完成。为了少用内存,指令的长度也是可变的。常用的指令要短一些,不常用的指令可以长一些。那个时候的计算机,想要用尽可能少的内存空间,存储尽量多的指令。

实际在 CPU 运行的程序里,80% 的时间都是在使用 20% 的简单指令。
RISC 架构的 CPU 的想法其实非常直观。既然我们 80% 的 时间都在用 20% 的简单指令,那我们能不能只要那 20% 的简单指令就好了呢?答案当然是可以的。因为指令数量多,计算机科学家们在软硬件两方面都受到了很多挑战。

在硬件层面,我们要想支持更多的复杂指令,CPU 里面的 电路就要更复杂,设计起来也就更困难。更复杂的电路,在 散热和功耗层面,也会带来更大的挑战。在软件层面,支持 更多的复杂指令,编译器的优化就变得更困难。毕竟,面向 2000 个指令来优化编译器和面向 500 个指令来优化编译器 的困难是完全不同的。 image.png 程序的 CPU 执行时间 = 指令数 × CPI × Clock Cycle Time
CISC的架构,其实就是通过优化指令数,来减少CPU的执行时间。而RISC的架构,其实是在优化CPI。因为指令比较简单,需要的时钟周期就比较少。

Intel 的进化:微指令架构的出现

让 CISC 风格的指令集,用 RISC 的形式在 CPU 里 面运行。
微指令架构的引入,让 CISC 和 RISC 的分界变得模糊了。 image.png 在微指令架构的 CPU 里面,编译器编译出来的机器码和汇 编代码并没有发生什么变化。但在指令译码的阶段,指令译 码器“翻译”出来的,不再是某一条 CPU 指令。译码器会 把一条机器码,“翻译”成好几条“微指令”。这里的一条 条微指令,就不再是 CISC 风格的了,而是变成了固定长度 的 RISC 风格的了。

这些 RISC 风格的微指令,会被放到一个微指令缓冲区里 面,然后再从缓冲区里面,分发给到后面的超标量,并且是 乱序执行的流水线架构里面。不过这个流水线架构里面接受 的,就不是复杂的指令,而是精简的指令了。在这个架构 里,我们的指令译码器相当于变成了设计模式里的一个“适 配器”(Adaptor)。这个适配器,填平了 CISC 和 RISC 之间的指令差异。

更复杂的电路和更长的译码时间:本来以为可以通过 RISC 提升的性能,结果又有一部分浪费在了指令译码上。

Intel 就在 CPU 里面加了一层 L0 Cache。这个 Cache 保存的就是指令译码器把 CISC 的指令“翻译”成 RISC 的微指令的结果。于是,在大部分情况下,CPU 都可 以从 Cache 里面拿到译码结果,而不需要让译码器去进行 实际的译码操作。这样不仅优化了性能,因为译码器的晶体 管开关动作变少了,还减少了功耗。

ARM 和 RISC-V:CPU 的现在与未来

功耗低、价格低

总结

多发射与超标量、超长指令字;超线程 (Hyper-Threading)技术,单指令多数据流(SIMD)技术;异常与上下文切换;CISC和RISC