CXD Linux Engineer

现代微处理器架构


前言

本篇文章是翻译于:

Modern Microprocessors A 90-Minute Guide!

以前在学校读过《计算机体系结构嵌入式方法》这本书,对CPU的体系结构有了一个比较清晰的了解, 然后偶然在网上看到这篇博文瞬间觉得简直就是这本书的一个全面总结,遂想把他翻译出来,就当是这本书的读书笔记吧!

现代微处理器架构

警告:本文章非权威,仅仅只是兴趣

好吧,如果你是CS专业毕业的并且大学期间学过硬件相关知识,但是不了解近几年中现代处理器的设计细节。那么这篇文章正适合你。

通常你应该不知道近些年来CPU发展的几个关键技术。。。

  • 流水线(超标量,OOO,VLIW,分支预测,predicated)
  • 多核和同步多线程(simultaneous multi-threading – SMT,超线程)
  • SIMD向量指令(MMX/SSE/AVX, AltiVec, NEON)
  • 缓存和存储器分层结构

不要害怕,本文将带你快速了解这些概念,让你在任何时候可以像专家一样谈论顺序执行和乱序执行之间的区别,超线程,多核和缓存结构等这些话题。 但要做好心理准备–本文非常简短只会点到为止,不会介绍过多细节。让我们正式开始吧。。。

不仅仅是频率

首先必须搞清楚的第一个问题是时钟频率和处理器性能之间的区别。他们是不一样的,先来看看几年前的处理器的性能和频率(上世纪90年代末):

频率 型号 SPECint95 SPECfp95
195 MHz MIPS R10000 11.0 17.0
400 MHz Alpha 21164 12.3 17.2
300 MHz UltraSPARC 12.1 15.5
300 MHz Pentium II 11.6 8.8
300 MHz PowerPC G3 14.8 11.4
135 MHz POWER2 6.2 17.6

表1 - 1997年左右的处理器性能

200MHz的MIPS R10000处理器、300MHz的UltraSPARC处理器和400MHz的Alpha 21164处理器之间的频率都不一样但是在运行大多数程序时的速度是一样的。 300MHz的Pentium II处理器在大多数情况下也有相同的速度,但是在处理浮点运算时的速度却只有一半。 PowerPC G3处理器的频率也是300MHz它在处理常规整型运算时比其他处理器快,但是浮点运算性能却远远低于前三名。 更极端的是IBM POWER2处理器仅仅只有135MHz的频率但是它的浮点运算性能与400HMz的Alpha 21164处理器相当,而整型运算能力却只有它的一半。

这是怎么回事?很显然这不仅仅只是因为时钟频率的不同–而是主要取决于处理器在每个时钟周期中是怎样工作的。

流水线和指令级并行

指令在处理器中是一个接着一个的执行,这样理解对吗?虽然这很容易理解但是实际情况并不是这样的。事实上从19世纪80年代中期开始多个指令可以同时并行执行。 想一想是怎样执行的 – 首先是取指、解码,接着在合适的功能单元中执行,最后将结果写入寄存器。根据这种方式,一个简单的处理器执行一条指令需要4个周期(CPI=4)。

sequential2

图1 - 顺序处理器的指令流

现代处理器将这些阶段叠加到一条流水线中,就像一条装配流水线。一条指令正在执行的同时下一条指令开始解码,下下一条指令则正在取指。。。

pipelined2

图2 - 流水线处理器的指令流

现在处理器的每个时钟周期可以执行一条指令(CPI=1)。在完全没有改变时钟频率的情况下将处理器速度提高了4倍。还不错吧?

站在硬件的角度,流水线的每个阶段由一些组合逻辑和可能访问寄存器组或者某种形式的高速缓存组成。流水线的每个阶段通过锁存器分开。 一个共同的时钟信号来同步每个流水线阶段之间的锁存器,以便于所有锁存器在同一时间捕获流水线的每个阶段产生的结果。也就是说使用时钟来驱动指令在流水线上流动。

在每个时钟周期的开始,流水线中的锁存器保存着当前正在执行指令的数据和控制信息,这些信息构成了该指令输入到流水线下一阶段的逻辑电路。在一个时钟周期中, 信号通过这一阶段的组合逻辑传输到下一阶段,在时钟周期的最后每个阶段产生的输出正好被下一阶段的锁存器捕获。。。

pipelinedmicroarch2

图3 - 流水线微架构

因为每条指令在流水线的执行阶段完成后产生的结果是可用的,下一条指令应该可以马上使用这个结果,而不是等到这个结果在流水线写回阶段被提交到目标寄存器后才使用。 为了实现一点,增加了被称为旁路的转发线路,将结果沿着流水线返回。。。

pipelinedbypasses2

图4 - 流水线微架构中的旁路

虽然每个流水线阶段看起来很简单,但是在执行这个关键阶段需要制造几组不同的逻辑(多条路径),为处理器必须有的每一种操作制作不同的功能单元。。。

pipelinedfunctionalunits2

图5 - 流水线微架构中的更多细节

早期的RISC处理器,例如IBM的801研究原型,MIPS R2000(基于斯坦福大学的MIPS架构)和原版的SPARC(伯克利RISC的衍生项目) ,都实现了简单的5级流水线和上面介绍的一样。 在同一时期主流的80386, 68030和VAX这些CISC处理器的大部分指令还是工作在顺序执行模式 – 因为RSIC处理器可以更加简单的实现流水线模式,因为精简指令集意味着这些指令绝大多数都是简单的寄存器到寄存器之间的操作, 不像x86, m68k 或者 VAX这些复杂指令集。结果导致20MHz的流水线模式的SPARC处理器比33MHz的顺序执行模式的386处理器运行速度还要快。 从那时开始每个处理器都实现了流水线模式,至少一定程度上的流水线化。David Patterson的这篇文章 1985 CACM article 对早期RISC研究项目做了一个很好的总结。

深度流水线 - 超级流水线

由于时钟频率限制于(其他因素除外)流水线中最长、最慢的那个阶段(译者注:一个时钟周期要大于流水线中最慢的那个阶段),逻辑门可以进一步细分每个流水线阶段,特别是最长的那个阶段,致使流水线变成更加细化的包含大量的短小阶段的超级流水线。 因此整个处理器可以运行在一个更高的时钟频率下!当然,此时每条指令需要花费更多的时钟周期来完成(时延),但是处理器仍然是每个周期完成一条指令(吞吐量),由于时钟频率更快所以处理器每秒可以执行更多的指令(实际性能)。。。

superpipelined2

图6 - 超级流水线处理器的指令流

Alpha架构特别喜欢这种技术,这也是为什么早期Alphas处理器拥有深度流水线在当时的时代下可以运行在如此之高的时钟频率下。 如今,现代处理器致力于限制逻辑门的数量以减少每个流水线阶段的延迟,每个流水线阶段大概12-25级门电路加上另外3-5个锁存器,但是大多数处理器都有非常深度的流水线。。。

Pipeline Depth Processors
6 UltraSPARC T1
7 PowerPC G4e
8 UltraSPARC T2/T3, Cortex-A9
10 Athlon, Scorpion
11 Krait
12 Pentium Pro/II/III, Athlon 64/Phenom, Apple A6
13 Denver
14 UltraSPARC III/IV, Core 2, Apple A7/A8
14/19 Core i*2/i*3 Sandy/Ivy Bridge, Core i*4/i*5 Haswell/Broadwell
15 Cortex-A15/A57
16 PowerPC G5, Core i*1 Nehalem
18 Bulldozer/Piledriver, Steamroller
20 Pentium 4
31 Pentium 4E Prescott

表2 - 常用处理器的流水线深度

通常x86处理器的流水线级数比RISCs(同时代的)的更多,因为他们需要额外的工作来解码复杂的x86指令集(下面会介绍更多)。 UltraSPARC T1/T2/T3 Niagara处理器是深度化流水线趋势中的例外 – UltraSPARC T1只有6级流水线,T2/T3是8级,这样做的目的是保持处理器核心尽可能的小(下面也会具体介绍)。

多发射 - 超标量

由于流水线的执行阶段是一组不同的功能单元,每个功能单元完成他们自己的任务,所以可以尝试多条指令在他们自己的功能单元中并行执行。 为了实现这一目标,取指和解码/调度阶段必须强化使他们可以并行解码多条指令,然后将他们分发到“执行资源”中去。。。

superscalarmicroarch2

图7 - 超标量微架构

当然,现在每个功能单元之间的流水线相互独立,他们甚至可以有不同数量的流水线级数。这可以使简单的指令更快的执行,从而减少延迟(我们很快会讲到). 由于这种处理器有很多不同的流水线级数,可以很正常的想到执行整型指令的流水线通常最短,存取指令和浮点指令流水线会有少量其他的流水线阶段。 因此,一个10级流水线的处理器使用10级来执行整型指令,存取指令可能有12或者13级,浮点指令可能有14或者15级。 流水线内部和流水线之间也会有一堆旁路,但是为了简化上图中省略了旁路。

在上面的例子中,处理器可能在一个时钟周期内发射3个不同的指令 – 例如1个整型指令,1个浮点指令和1个存取指令。甚至可以添加更多功能单元,实现处理器在每个时钟周期内可以执行2个整型指令,或者2个浮点指令, 或者使目标应用程序可以最高效率运行的任何指令组合。

在一个超标量处理器中,指令流大概像是这样的。。。

superscalar2

图8 - 超标量处理器中的指令流

这是非常棒的!现在每个时钟周期可以完成3个指令(CPI=0.33或者IPC=3,也可以写成ILP=3(instruction-level parallelism)- 指令级并行)。 处理器在每个时钟周期内可以发射,执行或者完成的指令数量称为处理器的带宽。

注意发射带宽一般小于功能单元的数量,因为不同的代码序列有不同的指令组合,我们的目标是达到每个时钟周期执行3条指令但是这些指令不可能总是1个整型指令,1个浮点指令和1个内存操作指令,因此功能单元的数量需要大于3个。

IBM POWER1处理器 - PowerPC的前代是第一个主流超标量处理器。之后大多数RISCs处理器(SuperSPARC, Alpha 21064)开始使用超标量。Intel也试图制造x86架构的超标量处理器 - Pentium处理器的原始版本 - 然而复杂的x86指令集成了一个很大的难题。

当然,处理器的深度流水线和多指令发射技术都在发展,所以超级流水线和超标量可以同时出现。。。

superpipelinedsuperscalar2

图9 - 超流水线-超标量处理器的指令流

如今实际上每一款处理器都同时是超流水线-超标量的,所以被简称为超标量。严格的说超流水线只是表示更深级数的流水线。

现代处理器之间的带宽差别很大。。。

Issue Width Processors
1 UltraSPARC T1
2 UltraSPARC T2/T3, Scorpion, Cortex-A9
3 Pentium Pro/II/III/M, Pentium 4, Krait, Apple A6, Cortex-A15/A57
4 UltraSPARC III/IV, PowerPC G4e
4/8 Bulldozer/Piledriver, Steamroller
5 PowerPC G5
6 Athlon, Athlon 64/Phenom, Core 2, Core i*1 Nehalem, Core i*2/i*3 Sandy/Ivy Bridge, Apple A7/A8
7 Denver
8 Core i*4/i*5 Haswell/Broadwell

表3 - 通用处理器的发射带宽

每款处理器的功能单元的实际数量和类型取决于目标市场。某些处理器的浮点运算资源更多(IBM的POWER处理器产品线),其他处理器则更加倾向于整型运算(Pentium Pro/II/III/M), 还有一些处理器则将资源投向SIMD向量指令(PowerPC G4/G4e),然而大多数处理器尽量使各种资源均衡。

显示并行 - VLIW

在那些不需要考虑向后兼容问题的使用场景中,使指令集本身被设计为可以并行执行的显示分组指令(explicitly group instructions)成为可能。 这种方法消除了在调度阶段需要的复杂的依赖性检查逻辑,可以使处理器的设计更加简单,体积更小,更容易的提高时钟频率(至少在理论上)。

在这种类型的处理器中,指令是指被分组过的更小的子指令集合,因此指令本身是非常长的,通常是128比特或者更多。所以VLIW的意思是超长指令字(very long instruction word)。 每条指令包含了多个并行操作的信息。

除了解码/调度阶段更加简单(因为只需要解码/调度每个组中的子指令)外VLIW处理器的指令流和超标量的很像。。。

vliw2

图10 - VLIW处理器的指令流

除了简化调度逻辑外VLIW处理器和超标量处理器非常相似。尤其是站在编译器的角度来看(下面会讲到)。

值得注意的是,大多数VLIW的指令相互之间没有关联。这意味着指令之间不需要依赖性检查,并且当缓存未命中时没有办法停止单独指令的执行只能停止整个CPU。因此编译器需要在依赖指令之间插入适当数量的时钟周期的间隔时间, 如果没有其他指令来填充这个间隔时间,甚至会使用nops(无操作,空指令)指令来填充。这使得编译器更加复杂化,因为在超标量处理器上通常是在运行时刻做这些操作的,为了节约处理器宝贵的片上资源编译器中这些额外的代码都是最小化的。

非VLIW设计仍然是商业领域内的主流CPUs,但是Intel的IA-64架构(应用于安腾处理器系列产品中)曾经试图取代x86架构。Intel将IA-64称为EPIC设计,意思是“显示并行指令计算”(explicitly parallel instruction computing),但是实际上就是VLIW的基础上加上智能分组(保证长期兼容性)和分支预测功能。 图形处理器(GPUs)中的可编程着色器有时候也采用VLIW设计,同时还有很多数字信号处理器(DSPs),也有Transmeta(全美达)这样的公司使用VLIW。

指令依赖和延迟

流水线和超标量能够发展多久?如果5级流水线可以快5倍,为什么不制作20级流水线?如果超标量每秒发射4条指令可以完美运行,为什么不发展为每秒发射8条指令?更进一步,为什么不制作一款50级流水线并且每个时钟周期发射20条指令的CPU?

好吧,考虑下面俩条语句。。。

a = b * c;
d = a + 1;

第二条语句依赖于第一条语句的结果 – 处理器不可能在第一条语句执行完产生结果之前就执行第二条语句。这是一个非常严重的问题,因为相互依赖的指令不能并行执行。因此多发射在这种情况下也不能使用。

如果第一条语句是一个简单的整型加法运算对于单发射的流水线处理器是可行的,因为整型加法的运算非常快,第一条语句的结果可以及时反馈(使用旁路)给下一条语句。但是对于多发射的情况,则会浪费几个周期来完成这个运算,因为没有办法在一个时钟周期的间隔内将第一条指令的结果传送给已经到达执行阶段的第二条指令。 所以处理器需要停止执行第二条指令直到第一条指令的结果可用,通过在流水线中插入气泡(bubble)来实现停顿。

当一条指令从执行阶段到它的结果可以被其他指令使用这之间间隔的时钟周期数量称为指令的延迟(latency)。流水线越深、级数越多指令延迟就越长。所以一个很长的流水线并不会比一个短流水线的效率高,由于指令之间的依赖会使越长的流水线中填充越多的气泡(bubble)。

站在编译器的角度来看,现代处理器的典型延迟时间范围从1个时钟周期(整型操作)到3-6个时钟周期(浮点加法、乘法运算可能相同或者稍微长一点),在到十几个时钟周期(整型除法)。

对于内存加载指令来说延迟是一个非常麻烦的问题,部分原因是他们通常发生在代码序列的早期,导致很难使用有效的指令来填充延迟,另外一个重要的原因是他们都是不可预测的 - 加载延迟的时间很大程度上取决于访问缓存是否命中(我们稍后很提到缓存)。

注:在相关但意义不一样的地方使用”latency”(延迟)这个词可能会造成误解。我们这里谈论的延迟是从编译器的角度来说的,但是对于硬件工程师来说延迟的意思是一条指令在流水线中执行完成所需要时钟周期(流水线级数)。所以硬件工程师会说一个简单的整型流水线的延迟是5但是吞吐量是1。 然而从编译器的角度来说他们的延迟是1因为他们的结果可以在下一个周期中使用。编译器的角度更加通用并且甚至在硬件手册中也会使用。

分支和分支预测

流水线的另外一个关键问题是分支。考虑下面这段代码。。。

if (a > 7) {
    b = c;
} else {
    b = d;
}

这段代码类似下面这种形式:

  cmp a, 7    ; a > 7 ?
    ble L1
    mov c, b    ; b = c
    br L2
L1: mov d, b    ; b = d
L2: ...

现在想象流水线处理器执行这段代码。当第二行的条件分支在流水线中到了执行阶段,处理器应该已经取指并解码了下一条指令,但是应该是那一条指令?他是取指并解码if分支(3,4行)还是else分支(第5行)? 一直到条件分支到达执行阶段才能知道答案,但是在一个深度流水线处理器中可能需要间隔几个时钟周期。并且它不能只是等待–处理器平均会在每6条指令中遇到一个分支,如果在每个分支下都等待几个时钟周期那么在一开始为了提高性能而使用流水线就没有可能。

因此处理器必须做出猜测。处理器会在执行这些指令的开始猜测和推测取指的路径。当然他不会实际提交(写回)这些指令的执行结果直到分支的结果已知。糟糕的是如果猜测错误这些指令不得不取消而这些时钟周期将会被浪费。但是如果猜测正确则处理器可以继续全速运行。

问题的关键是处理器应该怎样进行猜测。两种方案可供选择。第一,编译器应该可以标记这个分支来告诉处理器执行哪一条路径,这被称为静态分支预测。理想的情况是指令中有一个位来标记预测分支,但是对于早期的架构没有这个选项, 所以可以使用一个约定来实现,例如将预测会被执行的分支放在后面,不会被执行的分支放在前面。更重要的是这种方法要求编译器足够智能够正确预测,对于循环来说这很容易但是对于其他分支来说就可能非常困难了。

另一种方法是处理器在运行时刻做出判断。通常使用一个片上分支预测表来实现,表中保存着最近执行过的分支的地址并且用一位来标记每个分支在上一次运行中是否被执行。实际上,大多数处理器使用两位来标记,这样可以避免单个偶然事件的发生影响预测结果(尤其是在循环边缘的)。 当然这个动态分支预测表需要占用处理器片上的宝贵资源但是分支预测是如此重要所以这点资源是值得的

不幸的是即使是最好的分支预测技术有时也会预测错误,从而导致一个深度流水线上的很多指令都要被取消,这被称为分支预测惩罚。Pentium Pro/II/III处理器是一个很好的例子 – 它有12级流水线因此错误预测惩罚是10-15个时钟周期。 即使使用非常智能的动态分支预测器其正确率可以达到惊人的90%,但是由于高昂的分支预测惩罚也会使Pentium Pro/II/III处理器浪费掉30%的性能。换句话说Pentium Pro/II/III处理器三分之一的时间都在做没有用的工作,而是再说“哎呀,走错路了”.

现代处理器致力于投入更多的硬件资源到分支预测上,试图提高预测的准确率以减少错误预测惩罚的开销。很多记录每一个分支的方向并不是孤立的,而是由两个分支的执行情况来决定的,这被称为两级自适应预测器。有些处理器保存了一个全局的分支执行历史,而不是每个单独分支的分散的执行记录,以试图检测分支之间的任何关联性即使他们在代码中相隔较远。 这被称为gshare或者 gselect 预测器。现代最先进的处理器通常实现了多个分支预测器然后在他们之间选择那个看起来在每个单独分支上预测最精准的一个。

然而即使是最先进的处理器使用了最好的、最智能的分支预测器也只能将正确率提升到95%,仍然会由于预测错误而失去相当一部分的性能。规则非常简单 – 流水线太深则收益会减少,因为流水线越深你就必须预测越多的分支,所以错误的可能性也越大,并且错误预测惩罚也越大。

使用谓词指令消除分支

条件分支是一个大问题所以如果能完全去掉他们那就太好了。但是不可能在编程语言中取消if语句,所以怎样才能消除分支了?答案就在一些分支的使用方法中。

再次回到上面的例子中,5条指令中有两个分支,其中一个是无条件转移分支。如果可以给mov指令做一个标记告诉他们只在某个条件下执行,则代码可以简化为:

cmp a, 7       ; a > 7 ?
mov c, b       ; b = c
cmovle d, b    ; if le, then b = d

这里引入了一条新的指令cmovle – “如果小于或等于则移动”。这条指令会正常执行但是只有在条件为真的时候才会提交执行结果。这种指令称为谓词指令,因为它的执行被一个谓词控制(判断真/假)。

使用这种新的谓词移动指令,代码中的两条消耗较大的分支指令都可以被移除。另外聪明的做法是总是首先执行mov指令然后如果需要则覆盖mov指令的结果。同时也提高了代码的并行度 – 第1、2行的代码现在可以并行执行。结果是提速了50%(2个周期而不是3个)。 更重要的是消除了分支预测错误时的巨大的错误预测惩罚。

当然如果ifelse语句中的代码块非常大,则使用谓词会比使用分支执行更多的指令,因为处理器会将两条路径上的代码都执行一遍。通过多执行几条指令以消除分支是否值得这是一个棘手的决定–如果代码块很小或者很大则这个决定很容易, 但是对于那些中等大小的代码块则需要负责的权衡,这种情况在优化时必须考虑到。

Alpha架构在一开始就有条件移动指令。MIPS, SPARC 和 x86是后面在加上的。在IA-64中Intel尽力将几乎所有指令变成谓词指令,试图显著减少循环内部中的分支问题尤其是那些不可预测的分支,例如编译器和OS内核中的分支。 有趣的是许多手机和平板中使用的ARM架构是第一种全谓词指令集的架构。更有趣的是早期的ARM处理器只有很短的流水线因此它的错误预测惩罚相对较小。

指令调度,寄存器重命名和OOO

如果是分支并且指令延迟很长则会在流水线中产生气泡,但是这些空周期应该可以被用来做其他工作。为了实现这一点程序中的指令必须重新排序,当一条指令在等待时可以执行其他指令。例如在上面的那个乘法例子中可以在两条语句之间插入程序中其他的语句。

有两种方法可以实现指令调度,一种方法是在运行时刻在硬件中对指令重新排序。在处理器中实现动态指令调度(重新排序)意味着处理器的指令调度逻辑必须增强,使他可以在指令组中查找并乱序分发指令使处理器功能单元可以得到最好的利用。 不出所料这就是所谓的乱序执行,或者简称为OOO(有时也被写为OoO或者OOE)。

如果处理器打算乱序执行指令,则它需要记住这些指令之间的依赖性。通过使用一组重命名寄存器而不去处理原始架构定义的寄存器可以很容易实现这个目的。 例如将寄存器中的值存储到内存中,接着加载内存中的其他值到相同名字的寄存器中表示不同的值但是不需要加载到同一个物理寄存器中。更进一步说,如果这些不同的指令映射到不同的物理寄存器中则他们可以并行执行,这就是实现乱序执行的方法。 因此处理器必须时刻保存着指令在执行过程中和指令所使用的物理寄存器之间的映射。这个过程叫做寄存器重命名。一个额外的好处是可以利用更多的寄存器在代码中提取更多的可以并行执行的指令。

所有的这些依赖分析、寄存器重命名和乱序执行都需要在处理器中增加大量的复杂逻辑,使处理器的设计更加困难、芯片面积更大、功率更大。 这些额外的处理逻辑特别耗电因为这些晶体管总是处于工作状态,不像处理器中的功能单元至少有时候可以处于闲置状态(甚至可能关闭)。 但是乱序执行的优势在于软件不需要重新编译就可以获得一定的性能提升,但通常不是所有软件。

另一种一劳永逸的方法是通过编译器来重新安排指令的执行顺序。这叫做静态或者编译时刻指令调度。依赖于编译器的指令安排使得重新排序的指令流可以简单有序的被送入处理器的多发射器中。这样就避免了处理器中为了实现乱序执行而增加的复杂逻辑,使得处理器设计简单、功率变小并且芯片面积更小, 这就意味着更多的处理核心或者缓存可以放在相同面积大小的芯片中。

使用编译器实现的方法比硬件实现还有另外一个优点是 – 他可以看到程序执行时的更底层细节,并且可以推测分支的多条路径而不只是一条,这对于分支预测来说很重要。但是不能对编译器抱有太大希望,因为它不可能总是使得一切完美。 如果没有支持乱序执行的硬件,当编译器预测失败例如缓存未命中则会导致流水线停滞。

大多数早期超标量处理器都是顺序执行的设计(SuperSPARC, hyperSPARC, UltraSPARC, Alpha 21064 & 21164, 早期的Pentium)。早期乱序设计的处理器有MIPS R10000, Alpha 21264 和基本上整个POWER/PowerPC产品线。如今几乎所有高性能处理器都是乱序设计,但是有几个明显的例外UltraSPARC III/IV, POWER6 和 Denver。 大多数低功率,低性能的处理器例如Cortex-A7/A53和Atom都是顺序设计的因为乱序逻辑消耗了太多的电能但是性能的提升却很有限。

能量墙和ILP墙

ILP –(instruction-level parallelism)– 指令级并行

奔腾4处理器严重的电能消耗和散热问题证明了时钟频率是有极限的。事实证明电能消耗的增长速度远比时钟增长速度快 – 无论使用任何芯片技术,20%的时钟速度提升会导致能源消耗提高50%, 因为不仅仅是晶体管的开关频率提高20%,也需要提高电压才能驱动高速电路中的信号以保证很短的定时要求,当然前提是电路全部工作在提高的时钟频率下。 虽然功率的增加是随着时钟频率的线性增加,但是电压却是平方增加,导致在很高的速率下受到“三重打击”(f*V*V)。

更糟糕的是除了正常的开关电能消耗外还有一小部分的电能泄漏,因为当晶体管关闭流过它的电流并不会完全降低到0. 就像正常的电能消耗一样,这种电能泄漏也会随电压的增高而增加。这还不是最糟糕的,电能泄漏的增多会引起温度的升高使粒子运动加剧,导致硅中的高能电子增多。

如今得出的结果是,现代CPU的时钟相对增加30%会使功率增大一倍,发热量也会增加一倍。

heatsink

图12 – 现代PC处理器的散热器

功率的少量提高是可以接受的,但是当提高到一个确定的点时就会导致处理器的电源和散热问题不可控,目前这个点在150-200瓦特之间。 因为即使电路可以在高速时钟频率下工作但是也没有实际的方法来提供更多的电能并使芯片温度冷却下来。这被称为能量墙

Pentium 4,IBM的POWER6和最近代的AMD的Bulldozer/Piledriver等处理器都过于追求时钟速度的提升,导致他们很快就遇到了能量墙,从而发现没有办法将时钟速度提高到他们想要的速度上。 所以他们开始降低时钟速度转而开发更加智能的指令级并行处理器。

因此纯粹增加时钟的速度并不是一个好的决策,当然这个结论也适用于移动设备,如笔记本、平板和手机,它的会更快的到达能量墙。由于电池容量的限制并且通常没有散热器所以笔记本的能量墙在50W左右,平板的是10W,手机的是5W。

由于增加时钟速度会遇到瓶颈,那单纯的增加指令并行度是一个正确的方法吗?也不是。增加更多的并行度也会遇到瓶颈,因为正常的程序中由于加载延迟、缓存未命中、分支和指令间的依赖等因素,不可能在程序中找到更多的可以并行的指令。 可用的指令级并行限制称为ILP墙

早期的POWER、SuperSPARC和MIPS R10000等处理器过于追求ILP,从而很快发现提取更多的并行指令变得非常困难,而复杂的逻辑阻碍了时钟速度的提高。 导致这些处理器不再追求ILP,转而去提高时钟速度。

一个4发射超标量处理器希望每个周期中有4个延时和依赖都符合要求的单独指令可以被执行。实际上这基本是不可能的,特别是加载延迟达到3-4个周期。 目前现实世界中的一个单线程应用程序的指令级并行度大约可以每个周期执行2-3条指令。实际上现代处理器在SPECint标准下的ILP小于2条指令每周期。 而且SPEC标准的程序比现实世界中的大型应用程序更简单。某些应用程序展现出更好的并行度,例如科学计算但是这些程序不能代表主流应用程序。 有些类型的代码甚至维持每个周期执行1条指令都非常困难,例如指针追逐(pointer chasing)。对于这些程序瓶颈在内存系统上,因此又有另一种墙–内存墙(后面会讲到)。

x86介绍

x86也会遇到上面那些问题,但是Intel和AMD是怎样让一个已经存在了35年的架构在CPU不断发展的过程中仍然保持竞争力?

早期的奔腾是一款超标量x86指令集的处理器,它是工程界的一个奇迹因为对于复杂并且混乱的x86指令集来说实现超标量是极其困难的。复杂的寻址模式和最小量的寄存器数量并且指令之间潜在的依赖性意味着只有少数指令可以并行执行。 x86阵营为了和RISC架构竞争,他们必须找到一种方法来“简化”x86指令集。

Intel和NexGen公司的工程师各自独立(在同一时间)提出了解决方案–动态将x86指令解码为简单指令,一种类RISC的微指令。这种微指令可以运行在一个快速的、RISC风格的寄存器重命名、乱序执行、超标量的核心上。 这种微指令通常叫做μops(读作“micro ops”)。大多数x86指令解码成1,2或者3个μops,更复杂的x86指令需要更多的微指令。

对于这些分离的超标量x86处理器,很明显寄存器重命名技术是至关重要的因为32位模式下的x86架构只有8个寄存器(64位模式下增加了另外8个寄存器)。这和RISC架构有很大区别,RISC架构已经提供了很多寄存器,寄存器重命名只起到辅助作用。 然而,通过智能的寄存器重命名技术RISC架构上的所有技术都可以移植到x86上面来,但是有两个例外:静态指令调度(因为微指令隐藏在x86指令下面,对于编译器是不可见的)和利用大量寄存器组来减少内存访问。

x86处理器的工作方式就像下面这样。。。

riscyx862

图13 - 一种“RISCy x86”分离微架构

NexGen公司的Nx586和Intel的Pentium Pro处理器是第一个采用x86指令分离的微架构设计,如今所有现代x86处理器使用这种技术。当然和各种各样的RISC处理器一样他们在流水线和功能单元等方面的设计细节是不同的。 但是将x86指令翻译为类RSIC指令的基本思想是一样的。

当前的x86处理器甚至将翻译过后的微指令储存在一个很小的缓存当中,命名为L0微指令缓存。用于避免在循环当中一次次重复翻译相同的x86指令,这样不仅节约了时间而且减少了能量消耗。 这就是为什么Core i*2/i*3 Sandy/Ivy Bridge 和 Core i*4/i*5 Haswell/Broadwell处理器的流水线深度写成14/19 – 因为有14级是运行在L0微指令缓存中(通常情况下),但是还有19级运行在L1指令缓存中需要将x86指令翻译为微指令。

x86的这种类RISC微指令方案使得现代x86处理器对于带宽这个概念变得非常模糊,因为处理器内部为了便于跟踪经常将一组通用的微指令组成一个集合(例如加载和相加集合或者比较和分支集合)。 例如Core i4/i5 Haswell/Broadwell处理器每个周期可以最多解码5条x86指令,最大产生4个微指令集合,然后存储到L0微指令缓存中。因此每个周期可以取4个微指令集合并执行完成。 或者每个周期取8个单独微指令在不同的流水线中执行。所以Haswell/Broadwell处理器的带宽到底是多少?从处理器的硬件架构来看它的带宽是8,因为每个周期可以取8条微指令并执行完成, 从微指令的集合来看它的带宽是4,因为每个周期可以完成4个微指令集合,而从原始的x86指令集来看它的带宽是5,因为每周期执行5条x86指令。 当然带宽这种概念只是在学术上面讨论,因为实际程序的执行不可能达到这么高水平的指令集并行度(ILP)。

类RISC风格的x86家族中最有趣的一个成员是Transmeta公司的Crusoe处理器,它将x86指令翻译为内部的VLIW形式的指令,而不是内部微指令,并且使用软件在运行时刻进行翻译,非常像Java的虚拟机。 这种方法可以使处理器设计成简单的VLIW形式,而不需要硬件实现解码和寄存器重命名来解耦复杂的x86指令,并且不需要任何超标量调度和乱序执行的逻辑。 这种基于软件的x86指令翻译相比于硬件翻译降低了系统的性能,但是却可以大大简化芯片的设计并且速度可以非常快也大大减小了功率的消耗。 一个600MHz的Crusoe处理器可以媲美当时500MHz频率的工作在低功率模式(300MhZ的时钟频率)下的Pentium III处理器,但是它的功率和散热比Pentium 这种处理器可以应用于笔记本和掌上电脑等这种电池容量非常小的设备上。如今x86处理器也有应用于低功率领域经过专门设计的处理器。 例如Pentium M和他后面的升级版本,虽然没有必要使用Transmeta公司的基于软件翻译的方法, 但是NVIDIA公司也使用了非常类似的方法设计出应用于低功率但是要求高性能领域的Denver ARM处理器。

线程 - SMT,超线程和多核

前面已经提到增加指令级并行的方法由于实际程序中难以找到更多的并行指令而受到严重制约。因为这个原因即使使用最好的乱序执行超标量处理器在加上智能的编译器静态指令调度优化, 执行现实世界的程序由于加载延迟、缓存未命中、分支和指令间依赖这些因素的制约也很难达到平均每个周期执行2-3条指令。在同一个周期中发射多条指令只发生在最多几个周期的短暂时间内,大量周期是在执行低ILP的代码,因此很难达到峰值性能。

如果不能在当前运行的程序中找到额外的独立指令,还可以在另一个潜在的资源中找到独立指令 – 其他正在运行的程序,或者当前进程中的其他线程。同步多线程Simultaneous multi-threading(SMT)这种处理器设计技术正是利用了线程级的并行。

再说一次,这种方法也是利用有效的指令来填充流水线中的气泡,但是这一次不是使用当前程序中的指令(这种指令很难找到)而是使用正在运行的多线程中的其他线程的指令,他们都运行在一个处理器核心上。 因此单个SMT处理器表现的好像是同时有多个独立处理器,就像一个真实的多处理器系统一样。

当然一个真实的多处理器系统也同时执行多个线程–但是每个处理器上只有一个线程在运行。多核处理器也是这样的,他是将两个或者多个处理器核心放在同一个芯片上,而其他地方和传统的多处理器系统一样。 相反一个SMT处理器仅仅使用一个物理处理器核心在这个系统上呈现出两个或者多个逻辑处理器。这使得SMT处理器在芯片面积、造价、功率和散热上比多核处理器更加高效。 当然可以实现多核处理器上的每个核心都是SMT设计。

站在硬件的角度看,实现一个SMT处理器需要复制处理器中表示一个线程运行状态的所有器件,例如程序计数器、架构可见的寄存器(不是重命名寄存器)、保存在TLB中的内存映射等等。 幸运的是这些器件只占用整个处理器硬件的一小部分。真正庞大、复杂的部分例如解码器和调度逻辑、功能单元和缓存这些都是线程之间共用的。

当然处理器也必须在任何时刻跟踪哪条指令和哪个重命名寄存器属于哪个线程的,但是这只需要在整个处理器核心逻辑上增加少量复杂度。因此只需要花费少量成本而得到了整体性能的提升。 SMT处理器的指令流就像下面这样。。。

smt2

图14 - SMT处理器的指令流

这实在太棒了!现在我们可以通过运行多线程来填满这些气泡,我们可以增加更多的功能单元并且可以实现真正的多指令发射。在某些情况下设甚至可能提高单线程的性能(例如那些ILP友好的代码)。

因此我们可以实现每周期发射20条指令的处理器吗?不幸的是,答案是否定的。

首先实现SMT的前提是同时有很多个程序在运行(不仅仅是空闲进程idle),或者一个进程中有多个线程在同时运行。从多处理器系统中获得的经验告诉我们不可能总是如此。实践中,至少对于桌面电脑、笔记本、平板、手机和小型服务器来说,同时有多个不同程序在运行的情况是很少见的。 因此通常机器上只有一个任务在执行。

例如数据库系统、图像视频处理、音频处理、3D图形渲染和科学计算等等这些应用可以很容易的利用并行处理,但是很不幸的是即使是这些应用大部分也没有 被编写成并行友好的代码。另外很多应用程序可以很容易的实现并行化,但是他们本质上并不能完全并行运行,因为瓶颈在内存带宽那里而不是处理器的问题。 所以增加第二个线程或者进程并没有多少用处,除非内存带宽也可以增加(我们很快将会讨论内存系统)。 更糟糕的是还有很多类型的软件根本就不能实现并行化,例如web浏览器、多媒体工具、语言翻译、硬件模拟等等。

事实上SMT设计中的多线程都是共享一个处理器核心和一组缓存,他和多处理器相比在性能上还是有很大差距的。 在SMT处理器的流水线中如果有一个线程总是占据处理器的某一个功能单元,而其他线程也需要这个功能单元,则会阻塞所有其他的线程, 即使其他线程仅仅只是偶尔使用这个功能单元。因此线程之间的平衡变得非常重要,最有效的利用SMT的方式是使程序高度差异化, 这样可以使线程之间不会经常竞争同一个硬件资源。而且多线程之间的缓存空间竞争可能会导致比运行单个线程的效率还要低, 尤其是那些对缓存大小高度敏感的程序,例如硬件模拟/仿真,虚拟机和解码高质量的视频。

对于某些应用SMT的性能实际上比同时只运行单个线程然后多线程之间进行传统的线程上下文切换时的性能低。但是程序的主要限制是内存延迟(不是内存带宽), 例如数据库系统、3D图形渲染和大多数通用代码受益于SMT,是因为它们可以更好的利用内存加载延迟和缓存未命中时的等待时间。 因此SMT的性能和应用程序之间有着非常复杂的关系。这也使得它很难得到市场的认可,因为它的性能有时比多核处理器还要高,有时又像一个残缺的多核处理器, 有时甚至比单处理器的性能还低。

奔腾4是第一个使用SMT设计的处理器,Intel称之为“超线程”。奔腾4处理器的SMT设计使性能提高-10% ~ 30%,依赖于不同的应用程序。 随后Intel逐渐避免使用SMT设计并开始向多核过渡。同一时期其他厂商也取消了SMT设计(Alpha 21464, UltraSPARC V),SMT渐渐失宠。 直到POWER5的出现,它采用双核四线程设计。然后Intel的I系列处理器都使用了双线程SMT设计,例如四核八线程处理器。 Sun公司在并行设计上表现的更加积极,他在UltraSPARC T1处理器上使用了8个核心并且每个核心上使用了4线程SMT设计,总共32个线程。 在UltraSPARC T2处理器上更是增加到了每核心8线程设计,然后UltraSPARC T3处理器上塞进16个核心,每个处理器上总共128个线程!

更多的核心还是更强大的核心

SMT可以使指令级并行转换为线程级并行,并且增加了单线程的性能特别是ILP友好的代码。你可能会问既然SMT设计要优于同样带宽的多核处理器,为什么多核处理器依然存在。

很不幸问题并没有这么简单。事实证明高带宽的超标量设计在芯片面积和时钟速度方面表现很差。 一个关键的问题是多发射的调度逻辑复杂度基本上和发射带宽的平方成正比,因为n个待执行的指令彼此之间必须都要进行一次比较。 虽然可以利用更加智能的工程技术来重新按排指令的执行顺序以减轻指令调度的负担,但是复杂度依然成n的平方倍增长。 因此一个5发射的处理器要比一个4发射处理器的调度逻辑复杂50%,而一个6发射的处理器要复杂2倍,以此类推。 此外一个带宽很高的超标量设计要求高度多端口访问的寄存器和缓存来满足多个指令同时访问的需求。 所有的这些因素导致了不仅仅是芯片体积的增加,而且大大增加了电路设计时的布线长度,严重限制了时钟速度。 所以一个10发射的处理器不仅比5发射的处理器更大而且还更慢,而我们梦想的20发射的SMT设计则更不可能出现。

由于SMT和多核的性能发挥非常依赖实际使用的目标程序,所以SMT和多核设计之间的取舍还是非常有意义的。

现今,一个典型的SMT设计都实现了多发射、乱序执行、多解码器和大而且复杂的超标量调度逻辑等等。 因此SMT核心在芯片面积方面都非常大。而使用相同的芯片面积可以实现更多简单的、单发射、顺序执行的核心(无论有或者没有基本的SMT)。 实际中,一个乱序执行的超标量SMT设计所需要的芯片面积可以用来放置半打的小型、简单核心。

现在我们知道不管是指令级并行还是线程级并行都会遭受收益递减的命运(不同程度上), 需要记住的是SMT实质上是将ILP变成了TLP,超标量的带宽越宽芯片面积增长的越快(还有设计复杂度和功率消耗), 一个明显的问题是平衡点在那里?带宽需要多大可以得到ILP和TLP之间最好的平衡?目前还在探索中。。。

chipsandybridge chipniagara3

图15 - 两种设计极端:CoreI*2 “Sandy Bridge” VS UltraSPARC T3 “Niagara 3”。

一种极端是Intel公司的CoreI*2 “Sandy Bridge”(上图左边),它是4核处理器,每个核心都是6发射,乱序执行、智能核心、2个线程的(图中的下边是共享的L3缓存)所以总共有8个线程。 另一个极端是Sun/Oracle公司的UltraSPARC T3 Niagara 3(上图右边),它包含16个小型,简化,2发射的顺序执行的核心(图中上下两边是共享的L2缓存),每个核心运行可以8个线程,总共128个线程, 但是这些线程相比CoreI*2要慢很多。他们的芯片面积都相同,晶体管的数量都是10亿个左右(假设他们的晶体管分布密度相同)。请看上图一个简化、顺序执行的核心是多么的小巧。


Comments

Content