序言
也许很多朋友都会对CPU的原理比较感兴趣,从网上大家可以搜集到“利用晶体管的开关闭合进行二进制运算或数据存贮”之类的东西,但是CPU的性能由何决定?CPU内部的设计师如何的?分支预测,乱序执行到底是什么?超长流水线,RISC又是何物?为什么酷睿的CPU频率低,但是比高频率的P4性能好那么多?这些我们应用中遇见的问题绝对不是几句简单的话语可以解释的。
写本文的目的,就是想让大家对CPU的了解更加深刻一些,更好的理解CPU到底是怎么运转的。但同样,考虑到仅仅是普及文,我个人不会对一些特别的技术和实现方式(比如逻辑电路的设计,编译原理等等)进行详细讲解,我们只需要知道这些东西通过一些设计就可以达到预期的效果,具体问题是工程师的问题。我会尽量让文章做到范围广,深度浅,把晦涩难懂的知识尽量具体化形象化,但不得不承认我也只是一个普通爱好者,所以错误也是难免的,希望大家共同交流,相互促进。
写本文的目的,就是想让大家对CPU的了解更加深刻一些,更好的理解CPU到底是怎么运转的。但同样,考虑到仅仅是普及文,我个人不会对一些特别的技术和实现方式(比如逻辑电路的设计,编译原理等等)进行详细讲解,我们只需要知道这些东西通过一些设计就可以达到预期的效果,具体问题是工程师的问题。我会尽量让文章做到范围广,深度浅,把晦涩难懂的知识尽量具体化形象化,但不得不承认我也只是一个普通爱好者,所以错误也是难免的,希望大家共同交流,相互促进。
这玩意实在太长了,不可能一帖,甚至几帖就可以说完,所以我决定利用连载的方式来写这篇文章。
内容大体分为三大章
1 处理器的数据线路和控制实现
2 流水线及其带来的问题
3 存储器结构的讨论
系统学习是比较繁琐的,但是想要了解一些东西必须要把基础打好,希望有心的朋友仔细读下去,我相信也许你会爱上计算机。
第一章结束
1.1 指令的执行过程(这里没有涉及到高级语言的编译,所以没有翻译指令)
CPU的性能由哪些因素决定?我想大多数人都会知道时钟周期这个概念,而另两条“指令数目”,“指令所需时钟周期数(CPI)”是并不为人所知的。而不同
的内部架构,也便深刻的影响了CPI的大小,所以有些处理器频率低但是处理任务更快。不过别急,一口吃一个胖子是不行的,在我们对CPU的架构进行探讨之
前,先来看看CPU是怎么处理一条指令的。
一条指令,首先会根据程序计数器(记录当前运行的指令地址,本身也是一种寄存器)从内存中去处指令,通过指令字段的内容,选择读取一个或者两个寄存器(在
汇编语言中,一条代码,即一个指令只能使用1或2个寄存器进行加减乘除运算,而寄存器,就是记录数据的一个东西,当然他还有更多用途,之后我们会提及)一
旦取得寄存器的操作数后,就可以对指令进行定性,大致分为三种,存储访问,算术逻辑和分支(分支也可以叫做跳转,一般的高级语言都用if语句,判断一些特
定条件从而进行两种或者更多的操作)存储访问指令需要对存储单元进行读出或者写入而访问寄存器;算术逻辑指令需要将ALU(运算器)计算得到的数据写回寄
存器;而分支指令则会通过对数据的比较,决定是否对下条指令地址进行修改(也就是是否进行if语句中的or后面的语句)
话外音:一条高级语言首先会被编译为汇编语言,之后通过指令集将其编译为二进制信号,从而让CPU明白它需要做哪些操作。
不要将缓存和寄存器的概念混淆,最好从百度百科上看看相应资料。缓存我们会在第三大章进行详细讨论。
1.2 数据通路的建立
什么叫数据通路?我想聪明的你一定知道,让一条指令顺畅执行下去,这就是一条数据通路,它包括指令,数据存储器,寄存器堆,运算单元和加法器(加法器是对
程序计数器进行调节的,也就是说当前指令执行完毕之后,加法器对程序计数器加上4,就可以切换到下一个指令地址,关于为什么加4这里不多说,在第三大章我
们会提及)。
程序计数器进行调节的,也就是说当前指令执行完毕之后,加法器对程序计数器加上4,就可以切换到下一个指令地址,关于为什么加4这里不多说,在第三大章我
们会提及)。
想要构成数据通路,首先需要一个存储程序指令的地方,那么我们就需要一个存储单元来存储程序指令,并根据所给地址提供指令。当前需要执行的指令一定要放在一个存储单元中,这就是我们之前提到的程序计数器(PC),然后同一个加法器增加PC的值使它指向下一条指令的地址。
得到了指令地址,就可以让ALU对指令进行处理,从而得到结果,而ALU想要进行计算,就需要从寄存器获得数据并写入寄存器,这样,我们就需要一个寄存器堆,来暂时存储这些指令地址,信息等等。
如果是储存访问指令和算术指令的话,那么接下来只需要将寄存器的数据显示,或者暂时储存就可以了,若是分支指令,就需要对ALU的数据和寄存器的数据进行
比较,如果为真,则执行if语句后紧跟的语句,如果为假,就会执行if语句中or后紧跟的语句,所以说分支指令总是延迟的,因为在没有得出指令的真假时我
们不能对接下来的指令进行处理。
比较,如果为真,则执行if语句后紧跟的语句,如果为假,就会执行if语句中or后紧跟的语句,所以说分支指令总是延迟的,因为在没有得出指令的真假时我
们不能对接下来的指令进行处理。
1.3 指令的处理实现
一条指令,通过拆分(解码器就是对指令进行拆分,重排等操作的,使用逻辑电路从而达到了一种特殊算法),因为ALU的处理能力是一定的,所以有些复杂指令不能在一个周期内处理完,而需要拆分或者让ALU利用更长的一个周期重复处理这段指令。
对于单周期实现,和1.2节中的数据通路非常相近,如果有不熟悉的朋友可以回阅上一节。
多周期的数据通路,与单周期通路最本质的区别有三点:
1 指令和数据使用相同的储存单元
2 只有一个ALU(也可以使多个,但目前不对其进行讨论),没有了加法器
3 每个重要的功能单元都加上了一些寄存器存储输出值,使后面的时钟周期得到需要的信息(这是寄存器的另一个用处)。
当一个时钟周期结束后,我们只需要将处理得到的结果反馈给PC程序计数器,那么它就可以再次发射指令,所以就没有必要安置加法器。
说了这么多,相信有心的朋友们就会发现,其实我们所说的单周期指令实现,就是早期的CISC复杂指令集计算机,而多周期就是RISC精简指令集计算机。而
CISC这个东东有两个致命缺点,第一,它不能够很好的进行流水操作(第二大章的内容,以后详细讲解),第二,因为指令的复杂程度不一,那么如果为了性能
而使用可变时钟周期的设计,会大大增加控制器等其他单元的设计问题,而若是使用固定的时钟频率,又会造成极大的浪费,所以CISC现在已经基本被抛弃了。
CISC这个东东有两个致命缺点,第一,它不能够很好的进行流水操作(第二大章的内容,以后详细讲解),第二,因为指令的复杂程度不一,那么如果为了性能
而使用可变时钟周期的设计,会大大增加控制器等其他单元的设计问题,而若是使用固定的时钟频率,又会造成极大的浪费,所以CISC现在已经基本被抛弃了。
1.4 异常
在CPU设计中,最具有挑战性的一个问题,是一个程序异常(打断程序运行,比如错误的保存了指令的结果),被中断(来自处理器外的异常,比如内存的读写错误),就比如算术溢出。很多业内人士并不区分开两者,都称之为中断。
那这些异常是如何处理的呢?为了对其进行处理,我们必须知道是那些指令引起的异常问题,目前有两种方法,第一种需要一个状态寄存器,其中有一个字段用于记
录异常产生的原因。另一种方法利用向量中断(用来控制转换的终端地址)这两种方式在这里我们不去展开细讲,更多的是为了下面几章做一点点铺垫。
录异常产生的原因。另一种方法利用向量中断(用来控制转换的终端地址)这两种方式在这里我们不去展开细讲,更多的是为了下面几章做一点点铺垫。
1.5 实例 奔腾处理器的内部架构
奔腾系列都是采用流水线设计,它让多条指令重叠从而达到更高的指令吞吐率,其时钟周期的长度有单个功能单元的延迟决定。这里我们暂且放一放,之后还会有详解。
Intel的IA 32指令集(指CPU能识别什么类型的指令)非常复杂,是对其实现控制电路的一大难点,虽然核心内容都是源于前几节的内容,但是想要出成品并不容易
IA 32的指令,有些可能用到几十个周期,甚至超过几百个周期的指令。例如,串行传送指令要求计算并修改两个不同的存储地址,并且存储一个字节串。而且他复杂的寻址模式也使其使其结构实现难度大大提高。
而Intel的工程师巧妙地运用多周期数据通路和微程序控制器(使用代码而不是01来表示控制的方法),这样,即使是需要周期数不同的指令,也会减少更多的周期损失(因为频率是固定的)
1.6 奔腾4的结构
超标量,这是在奔腾4系列CPU中引入的一种技术,简单来看,这种技术使得处理器可以有多条数据通路,每一条处理某一类型指令:存取指令,ALU计算,分
支。这样处理器便可以在一个周期内执行多条指令(当然这些都仅仅是皮毛,之后我们依旧会在第二大章进行讨论)。而所谓的微操作,便是利用某种技术,让每一
条指令分配给不同的数据通路,从而达到更高的效率。
支。这样处理器便可以在一个周期内执行多条指令(当然这些都仅仅是皮毛,之后我们依旧会在第二大章进行讨论)。而所谓的微操作,便是利用某种技术,让每一
条指令分配给不同的数据通路,从而达到更高的效率。
在奔腾4中,踪迹缓存技术就是来存储微指令的,这是一种解决方案,记录微指令将会被引入哪一条特定的数据通路。这种缓存技术比较复杂,我们会在第三大章中稍作了解。
奔腾4使用简单的硬联线控制和简单数据通路,结合踪迹缓存,获得了令人吃惊的时钟频率,当然还要得益于深度流水线的引入,不过这都是后话了。
第二章 流水线,更高更快
2.1 流水线初涉
相信对于大多数了解硬件的朋友,流水线早已不是什么陌生的词汇了,它并不难理解。
我们先回顾一下一般的数据通路如何处理指令,首先PC会将指令地址送给ALU,ALU进行处理将数据存储,然后PC值加4
,这样就可以进行下一个指令的处理。而流水线便是不间断的发射指令,原本一个指令需要得到结果才会被处理,而现在则是让指令充斥着整个数据通路,在ALU
处理第一条指令时,第二条指令已经进行取值,当第一条指令处理后,第二条指令立刻被送往ALU,这样就减少了非常多的时钟周期(一个指令需要经过取指令,
访问寄存器,ALU操作,访问数据,访问寄存器这五步)(再次进行解释,第二步中的访问寄存器是让ALU得到操作数据,第四步访问数据也就是得到ALU的
计算结果所必须的一些数据,例如a+b,我们需要知道a和b的值是多少。而访问寄存器就是对PC的操作)如果这样说还不明白的话,我们简单打个比方,我们
做饭,需要先把菜做好,然后去煮米饭,而流水线话操作时,我们可以一边煮米饭,一边做菜,这样就减少了整个做饭的时间。而假设煮米饭要10分钟,做菜要
20分钟,那么整个流水化操作后就需要20分钟,也就是说流水线操作的时间取决于需要时间最长的事情,在CPU中就是任务的执行周期数取决于最复杂,处理
速度最慢的指令(比如一个超长浮点运算)
,这样就可以进行下一个指令的处理。而流水线便是不间断的发射指令,原本一个指令需要得到结果才会被处理,而现在则是让指令充斥着整个数据通路,在ALU
处理第一条指令时,第二条指令已经进行取值,当第一条指令处理后,第二条指令立刻被送往ALU,这样就减少了非常多的时钟周期(一个指令需要经过取指令,
访问寄存器,ALU操作,访问数据,访问寄存器这五步)(再次进行解释,第二步中的访问寄存器是让ALU得到操作数据,第四步访问数据也就是得到ALU的
计算结果所必须的一些数据,例如a+b,我们需要知道a和b的值是多少。而访问寄存器就是对PC的操作)如果这样说还不明白的话,我们简单打个比方,我们
做饭,需要先把菜做好,然后去煮米饭,而流水线话操作时,我们可以一边煮米饭,一边做菜,这样就减少了整个做饭的时间。而假设煮米饭要10分钟,做菜要
20分钟,那么整个流水化操作后就需要20分钟,也就是说流水线操作的时间取决于需要时间最长的事情,在CPU中就是任务的执行周期数取决于最复杂,处理
速度最慢的指令(比如一个超长浮点运算)
附言:流水线增加的是CPU指令的吞吐率,而不是减少了单个指令执行的时间,而且因为某些特殊的问题,还会让单个指令执行时间增加,例如流水线寄存器的引入
2.2 流水线结构的冒险
我想当看完2.1节后大家一定会说,流水线竟然如此简单?确实流水线并不难理解,但是真正实现过程中人们发现了相当多的问题,会引起流水线的处理停顿,我们称为冒险。
结构冒险
这是第一种冒险,即硬件不支持多条指令在同一个时钟周期内执行。比如我们做菜的时候,因为家里的电器因为线路的原因只能工作一个,那么我们只能先煮米饭,再做菜了。
如果用CPU角度来考虑的话,如果在流水线中一个指令在该周期内需要从内存中取得数据,而同时另一条指令需要写入内存,这样,因为DDR内存在一个周期内
只能执行一次读或者写操作(数据总线只有一条,而对于GDDR3之后的显示卡内存,因为具备多条数据总线,可以对内存同时进行读写操作)那么这两条指令就
造成了结构冒险。
只能执行一次读或者写操作(数据总线只有一条,而对于GDDR3之后的显示卡内存,因为具备多条数据总线,可以对内存同时进行读写操作)那么这两条指令就
造成了结构冒险。
数据冒险
这是第二种冒险,在一个操作必须等待另一操作完成后才能进行时,那么就会造成流水线停顿。例如我们需要知道a和b,需要将两个数相加得到c的值,之后再对
c进行立即数操作(直接对数值进行加减乘除运算,而不需要读取被加数的寄存器地址),那么必须等a和b相加后得到的数据写到寄存器中才可以进行下一个操
作。
c进行立即数操作(直接对数值进行加减乘除运算,而不需要读取被加数的寄存器地址),那么必须等a和b相加后得到的数据写到寄存器中才可以进行下一个操
作。
控制冒险
第三种冒险,当处理器需要根据一条指令的结果作出决策,此时其他的指令可能在执行中。好比一个分支操作,我们需要知道是执行分支中哪一条指令才能进行下一步操作,这样接下来的指令就需要得到分支的结果才能进行处理。
2.2 有问题就要解决
既然流水线有那么多限制,为何不去简化它呢?或者说直接不流水。可是经过很多科学家的验证,流水线对程序提升的性能即使存在如此多的冒险,依旧客观,根据流水线加速比的公式,一个5步骤处理的CPU可以提升3~4倍的性能(实则5倍,但是因为有冒险)
不去解决流水线的冒险可以吗?对于像GPU这种高度并行,数据之间关联度低(也就是流水线的冒险度较低,不易发生流水线停顿)我们完全没有必要去理会,不过
通过对指令并行的提升(ILP Instruction-Level-Parallelism )比如co-issue(相信关注GPU核心的朋友对这个
词都会有所了解),让两个完全没有关联的数据进行流水操作从而提升性能,当然就目前来看ILP已经走到了尽头。
通过对指令并行的提升(ILP Instruction-Level-Parallelism )比如co-issue(相信关注GPU核心的朋友对这个
词都会有所了解),让两个完全没有关联的数据进行流水操作从而提升性能,当然就目前来看ILP已经走到了尽头。
而CPU则不然,因为我们运行的程序关联性较大,如果依旧无解决办法直接流水,那么损失的时钟周期是相当恐怖的,那么我们就来简单谈谈CPU设计中是如何减少这种冒险的。
数据旁路:这是一种解决数据冒险的方法,它使用内部的数据缓存直接提供缺少的数据,而不需等待该数据到达程序员可见的寄存器或内存才使用。实现它,需要用到直通技术。
为了直观,我们先举一个简单的例子,a+b=c,c+d=f,我们知道了a,b,d的值,要得到c的数据才能相加。首先我们的CPU会先计算出c的值,这时
候利用直通技术,将c的值直接传给下一个指令,而不需要等到该指令结束后由PC将地址发送,那么下一条指令也便无需等待,直接可以计算。
为了直观,我们先举一个简单的例子,a+b=c,c+d=f,我们知道了a,b,d的值,要得到c的数据才能相加。首先我们的CPU会先计算出c的值,这时
候利用直通技术,将c的值直接传给下一个指令,而不需要等到该指令结束后由PC将地址发送,那么下一条指令也便无需等待,直接可以计算。
重排指令:这里我们需要用一个简单的例子,我们需要计算a,b,而a=c+d,b=c+e,需要的数据都在寄存器上,因为要流水,那么就要重复获取c的值,
如果对这段高级语言进行编译,那么因为c的值需要在一个周期内取得两次(在正常编译后是这种情况),所以就会发生冒险。如果我们对加载c值的顺序进行调
整,就可以避免冲突。这也可以叫做乱序执行,也就是out of order
分支预测与分支延迟:这都是用来解决控制冒险的,因为需要得到分支的结果才可以进行下一步,那么在一些流水级超长的CPU中(比如奔腾4)会造成相当大的周期损失。分支预测,很简单,就是通过某种方式对分支的结果进行预测,那么我们也便无需等待分支的结果是真是假,直接执行下一步便可。分支延迟也很简单,我们先不理会分支的结果如何,对分支的两种可能都进行运算,等到分支得到结果之后再舍弃。
目前流行的都是分支预测,这里拆开还能将非常多,比如BTB,我们会在下几节进行深入探讨.
2.4 流水线的数据通路
首先,作为进阶的思考,我们还是回顾一下一般的单周期数据通路,我们经典的5阶段的CPU,分为IF取指令,ID指令译码(编译),EX指令执行,MEM
数据内存访问,WB写回。如果用5阶段的CPU进行流水,那么任意周期内,都会有一条指令充满这5个阶段,那么为了让指令能够得到不间断的发射,我们必须
增加更多的寄存器,当一条指令经过一个阶段后马上让PC值加上4,从而发射下一条指令,所以我们采取的方式是,在每个阶段之间都要加上流水线寄存器,比如
在IF和ID段中加入一个寄存器,那么该寄存器的作用便是得到IF的指令值,反馈,使得PC值加4,而且它还要做指令数据传输的工作,也就是将IF的数值
传送给ID段。那么流水线寄存器的频率就一定要是流水线本身频率的两倍,所以在一个周期中,前半段时间让PC值加上4,后半段则是传送数据给下一个阶段。
数据内存访问,WB写回。如果用5阶段的CPU进行流水,那么任意周期内,都会有一条指令充满这5个阶段,那么为了让指令能够得到不间断的发射,我们必须
增加更多的寄存器,当一条指令经过一个阶段后马上让PC值加上4,从而发射下一条指令,所以我们采取的方式是,在每个阶段之间都要加上流水线寄存器,比如
在IF和ID段中加入一个寄存器,那么该寄存器的作用便是得到IF的指令值,反馈,使得PC值加4,而且它还要做指令数据传输的工作,也就是将IF的数值
传送给ID段。那么流水线寄存器的频率就一定要是流水线本身频率的两倍,所以在一个周期中,前半段时间让PC值加上4,后半段则是传送数据给下一个阶段。
附录:superpipeline 超长流水线。
超长流水线,也称深度流水线,它是一种高级流水线技术,在这里我们简单来探讨一下。
首先要指出的是,超长流水线并不是指ALU更多的流水线。它的含义是将一个较短阶段(stage)的流水线划分成更多阶段。
其实在我一开始接触的时候也不明白,为什么划分更多阶段能给予良好的流水性能呢?其实这一点不难理解,因为这样,在一个周期内流水线内部的指令数会比原来
更多,同样因为划分的更加细,频率也更容易提升(因为流水线的频率取决于速度最慢的那一阶段),这样,在计算大量数据时,这种超长流水线就可以得到很好的
加速效果,但是,若是出现冒险,那么损失的时钟周期是非常可怕的。P4当中引入了大量的缓存(cache),目的就是让指令所需的数据能在cache中得
到而不去等待慢吞吞的内存(目前内存的频率都是200,DDR运用了DIMM上下沿并发和数据预取的技术从而等效达到了高频率,但是一个1.8G的CPU
依旧需要9个周期去等待内存数据)P4能达到较高的频率,也很大程度上依靠了这种高级流水线技术。
更多,同样因为划分的更加细,频率也更容易提升(因为流水线的频率取决于速度最慢的那一阶段),这样,在计算大量数据时,这种超长流水线就可以得到很好的
加速效果,但是,若是出现冒险,那么损失的时钟周期是非常可怕的。P4当中引入了大量的缓存(cache),目的就是让指令所需的数据能在cache中得
到而不去等待慢吞吞的内存(目前内存的频率都是200,DDR运用了DIMM上下沿并发和数据预取的技术从而等效达到了高频率,但是一个1.8G的CPU
依旧需要9个周期去等待内存数据)P4能达到较高的频率,也很大程度上依靠了这种高级流水线技术。
高级话题:性能的再次提升
流水线的开发,同样造就了一个概念,叫做指令集并行,。主要通过两种方式提高指令集的并行能力一个是增加流水线技术,另一种则是设计更多的内部元件从而在
一个流水线中的每级发射更多的指令。前者我们已经有所探讨,他就是P4中引入的superpipeline。后者就包括超标量设计,涉及到了流水线的宽
度。
我们现在深入探讨第二个话题。这种多发射技术,目前有两种比较聪明的实现方法,他们的区别就是编译器和硬件之间如何分工,由于分工主要在于某些决定是在编译期决定的还是在运行期决定的,所以前者称之为静态多发射,后者叫做动态多发射。
静态多发射处理器利用编译期从而对指令进行打包和处理各种特殊的流水线冒险,在静态多发射中,我们可以将多条指令看作一条超长指令,复杂指令。同样,因为
本身可以看作一条超长指令,那么它便被固定了长度,操作数。这就是所谓的VLIW,超长指令字,相信对R600了解的朋友一定不会陌生。
动态多发射处理器也被称之为超标量处理器,在比较简单的模型中,指令是按照顺序发射,每个周期可以发射1条或多条指令,或者说不发射。非常明显的是,这种
处理器要达到较好的性能非常依靠编译期对各条指令的调节,错开指令之间的依赖关系从而达到更高的发射速率。所以目前超标量处理器会更多的在指令排序方面下
工夫。
附录:多发射处理器的数据通路
非常简单,我们只需要在一般的多周期数据通路上增加更多的执行单元,比如ALU,对各种单元进行划分,比如A单元执行浮点,B单元执行整数,通过对寄存器的大小调节,便可以适应不同的指令。
非常简单,我们只需要在一般的多周期数据通路上增加更多的执行单元,比如ALU,对各种单元进行划分,比如A单元执行浮点,B单元执行整数,通过对寄存器的大小调节,便可以适应不同的指令。
第二章,完
第三章 层次结构存储器
1 导论
假如你是在做高数作业,为了快速计算,你会把积分表放在最显眼的地方,其次为了防止对一些积分变换的不熟练,你会放一个例题本在一旁,这样又不会的变换就可以直接查。而如果有一个题你怎么也做不出来,就值得去求教老师。
其实本来作业应该是我们自己做的,实在不会就去请教老师,但是借助了积分表和积分变换的例题就能更快的解决题目,而不需要去请教老师,因为请教老师太麻烦
太慢了。这样,在CPU当中也存在这种类似的环境,也就是说最能帮助你解决当前问题(指令最需要的数据)的应该优先拜访(优先访问),再或者说你有两本词
典,一本是英语5000实用词,另一本是牛津高阶英汉词典,查单词的时候,我们当然会先去查看实用词词典咯,大辞典查起来太慢了。
太慢了。这样,在CPU当中也存在这种类似的环境,也就是说最能帮助你解决当前问题(指令最需要的数据)的应该优先拜访(优先访问),再或者说你有两本词
典,一本是英语5000实用词,另一本是牛津高阶英汉词典,查单词的时候,我们当然会先去查看实用词词典咯,大辞典查起来太慢了。
下面就得引入必备的理论知识了
时间局部性:如果一个数据项被引用,那在不久之后它很有可能再次使用。比如在第一个例子中,比如一个循环指令,每次循环都需要一个常量B,那么B就非常容易被调用
空间局部性:如果某个数据项被引用,那么不久之后与它地址相近的数据项可能会被引用,也就是说数据间有相依性啦
层次结构存储器,也就是按照这两种原理所衍生的。现在的计算机就有缓存和内存之分(寄存器不属于层次结构存储器,它的目的是存储目前需要使用的数据,而不
是可能会使用到的数据,切记切记),缓存小而快速,一般都在CPU的uncore部分,内存就很容易理解了,在缓存和硬盘之间的,速度和容量在两者之间其
缓冲的东东。为什么速度快容量就要小呢?这是一个逻辑电路的知识,简单的来说就是为了得到更高的速度,那信号的发射频率必须提升,而为了得到更大的容量,
那么就必须保证信号传输中减少干扰。如果频率提高,干扰就会增强,所以两者比较难以统一。
是可能会使用到的数据,切记切记),缓存小而快速,一般都在CPU的uncore部分,内存就很容易理解了,在缓存和硬盘之间的,速度和容量在两者之间其
缓冲的东东。为什么速度快容量就要小呢?这是一个逻辑电路的知识,简单的来说就是为了得到更高的速度,那信号的发射频率必须提升,而为了得到更大的容量,
那么就必须保证信号传输中减少干扰。如果频率提高,干扰就会增强,所以两者比较难以统一。
存储器的层次结构可以由多级组成,但是数据复制每次必须在两个相邻层次间才可以进行,一个层次结构中存储信息的最小单元,我们称之为快或行
(line,cache
line,眼熟吧)如果处理器需要的数据出现在高层(速度快,容量小的cache)的块中,测称之为一次命中,而命中率就是指我们需要的数据在cache
中所占的比例。缺失率也就是cache中没有的数据占需求数据的百分比。
(line,cache
line,眼熟吧)如果处理器需要的数据出现在高层(速度快,容量小的cache)的块中,测称之为一次命中,而命中率就是指我们需要的数据在cache
中所占的比例。缺失率也就是cache中没有的数据占需求数据的百分比。
1.2 缓存的简单基础知识
cache,这是专业的叫法,其实就是一种层次结构罢了,早期的缓存都属于片外缓存,也就是集成在主板上的,现在的缓存都会放到CPU的Uncore中,
也就是非核心模块,cache本身属于一种超高速内存(可以如此理解),但是本身却和内存有较大的差距,因为cache属于高速量小的存储器,那么如何合
理利用这分宝贵的资源就是人们必须要掌握的了。
我们先来讨论一个比较简单的情况,假设内存中存储了1到10十个数据,如果我们这时候读取其中的4号数据,那么cache将会立刻将3,4,5这些数据调
入,因为根据之前我们所讲的时间空间局部性,这些临近的数据和数据本身都是很有可能被再次用到(数据第一次读取是去内存找而不是缓存,术语叫做强制性冲
突,下一次会有讲到哦)再来说一个,如果cache中有1~10数据中的3,4,5,如果CPU需要6号数据,而cache中没有,那么5,6,7数据就
会立即调入cache,根据局部性原理。
入,因为根据之前我们所讲的时间空间局部性,这些临近的数据和数据本身都是很有可能被再次用到(数据第一次读取是去内存找而不是缓存,术语叫做强制性冲
突,下一次会有讲到哦)再来说一个,如果cache中有1~10数据中的3,4,5,如果CPU需要6号数据,而cache中没有,那么5,6,7数据就
会立即调入cache,根据局部性原理。
这时候追求细节的朋友们可能会提问,CPU怎么知道cache中有没有所需的数据呢?如果在,又是如何找到的呢?最简单的方法就是给内存的每一个数据
(字)分配一个缓存的地址,每一个内存的数据地址对应一个确定的缓存地址(数据重复),也就是说,当我们读取内存发现cache中含有该数据那么就会去
cache中寻找,从而缩短的很多时间。这种简单的分配方式叫做直接映像。由于每个缓存的地址可对应的内存中多个不同地址,怎么才能知道所请求的数据是否
在缓存中呢?这就得引入标记这个概念了,标记必须包含能够判断缓存中的数据是否为所请求数据的地址信息。我们还需要一种办法来判断缓存中的确没有有效的信
息,我们就必须知道缓存中部分标记要被忽略,最常用的方法就是有效位,说明一个cache块中是否有有效地址。如果这个位置没有设置,就不能读取其内容。
(字)分配一个缓存的地址,每一个内存的数据地址对应一个确定的缓存地址(数据重复),也就是说,当我们读取内存发现cache中含有该数据那么就会去
cache中寻找,从而缩短的很多时间。这种简单的分配方式叫做直接映像。由于每个缓存的地址可对应的内存中多个不同地址,怎么才能知道所请求的数据是否
在缓存中呢?这就得引入标记这个概念了,标记必须包含能够判断缓存中的数据是否为所请求数据的地址信息。我们还需要一种办法来判断缓存中的确没有有效的信
息,我们就必须知道缓存中部分标记要被忽略,最常用的方法就是有效位,说明一个cache块中是否有有效地址。如果这个位置没有设置,就不能读取其内容。
1.3高速缓存的缺失
在接下来讨论实际系统的高速缓存之前,我们现在看一看控制部件是如何处理cache miss的。首先呢,我们必须通过控制器检测到缺失的发生,然后从低一级的缓存或内存中取出相应的数据,如果高速缓存命中,CPU继续照常处理,就好像什么事情都没有发生。
命中时,处理器的控制方法的修改并不太重要,而缺失时就必须附加一些工作了。cache的缺失处理由两部分协同完成,一个是处理器的控制器,另一个就是进
行初始化内存访问和重新填充缓存的独立缓存控制器。高速缓存的缺失导致流水线停顿(去看第二章),与流水线中断不同(比如你运行QQ,结果突然间把它关掉
了),中断发生时,系统要求保存所有寄存器的状态,而缺失发生时,我们在等待主存操作完成的期间,可以使整个机器停顿,本质上就是冻结临时寄存器和程序员
可见的寄存器(学编程吧,否则我讲了也不会明白)的内容,而缓存的缺失只是流水线停顿的一种情况,所以也是非常简单的。
作为硬件讨论的基础我们依然要讨论一下对于多周期或流水线的数据通路(进阶,必须把第一章的看完),指令发生缺失时将如何处理?如果去一条指令导致缺失。
那么指令寄存器的内容无效。为了把正确的指令取到高速缓存中,必须让存储器执行一次读操作,由于是在流水线中,在指令执行的第一个周期,程序计数器加一个
增量,因此产生高速缓存指令缺失的指令地址等于程序计数器减4.一旦有了地址,就可以只是主存执行一次读操作,之后等待存储器的相应,然后把数据写入高速
缓存。
说了这么多估计大家都已经晕掉了,下面我们再来条理地回顾一下高速缓存去指令缺失的处理步骤
1 把最初PC的值送到存储器
2 指示主存执行读操作
3 写高速缓存,把从存储器中读到的数据写入高速缓存的数据部分,把地址的高位写入标记域,设置有效位
4 重启第一步,它将重新取指令,这次发现指令在高速缓存中。
1.4 写操作
写操作稍微有些不同。假设有一个存储指令,我们只把数据写入高速缓存,那么写入高速缓存后,主存和高速缓存相应位置的值将会变化。在这种情况下,高速缓存
和主存称为不一致。保持一致的最简单方法就是在缓存和主存中都写入数据,也就是写通过(write through)。
尽管这种方法十分简单,但是并不能提供良好的性能,所以,写缓冲(write
buffer)就出现了。当一个数据在等待被写入主存时,先把它存放在缓冲器中。在把数据写入高速缓存和写缓冲后,处理器继续执行指令。当写入主存操作结
束后,写缓冲里的内容也将被释放。如果写缓冲满了,那么当处理器执行到一个写操作时,就得先停下来知道写缓冲有一个空的位置。当然,如果存储器完成写操作
的速度比处理器的速度慢,怎么缓冲也没有用。
buffer)就出现了。当一个数据在等待被写入主存时,先把它存放在缓冲器中。在把数据写入高速缓存和写缓冲后,处理器继续执行指令。当写入主存操作结
束后,写缓冲里的内容也将被释放。如果写缓冲满了,那么当处理器执行到一个写操作时,就得先停下来知道写缓冲有一个空的位置。当然,如果存储器完成写操作
的速度比处理器的速度慢,怎么缓冲也没有用。
写通过机制还有一种变通的方法是写回(write back),采用写回的方法,当一个写操作产生时,新的数据仅仅被写道高速缓存的块中,只有当修改过的块被替换时,才把它写到底层的存储结构中去。
附言:其实还有很多很多操作方式,我累了。。。。。只是说了说常见的。。。。。。
11月8日发烧,带病坚持= =低调
(实在不忍心继续停下了,顺便打算给图形学简单开个头,希望大家捧个场)
1.5 块的定位方式与cache缺失
一直到现在,我们讨论的将块放入cache中,都是用最简单的定位方式,即一个块只能放到高速缓存中的一个位置,也就是直接映射(因为存储器中任何一个块
的地址都被直接映射到更高层的cache中),事实上强大的工程师们还有非常多的方式来安排块,直接映射属于一种极端,一个块就是直接映射,这时一个块被
精确的放在一个位置中。
另一种极端方式则是,一个块可以放在cache中的任何位置。这种方式就是所谓的全相联(因为存储器中的块可以和cache中的任何一块联系)。在全相联
的cache中要找一个给定的块,由于该块可能被放在任何位置,必须检测高速缓存的所有项。为了使检测更加简洁,它是由与cache中美向相关的一个比较
器并行处理完成的,这些比较器增大了晶体管的开销,因为全相联只是用那些块的数量较少的cache。
介于两种极端的设计叫做组相联。在一个组相联的cache中,每个块可以被放在固定数量(2个以上)的位置上,每个块有n个位置可以放的组相联cache
被称为n路组相联cache。。一个n路的组相联cache有许多组构成,每个组中都有n个块,存储器的每个块由下标域对应到cache中唯一的组,并可
以放在此组中的任何一个块上。因此组相联映射是把直接映射和全相联映射结合起来的妥协措施,这样每个块可以直接映射到一个组,然后检测该组中的所有块是否
匹配。
将这么多有毛用?大家别急,现在我们再次回顾一下之前的东东。
在直接映射中,一个存储块的位置是如下给出的:
块号——cache中块的个数
而组相联则是:
块号——cache中组的个数
由于全相联中所需要的块可以放在任何地方,所以要检测cache中所有块的所有标记。
而组相联的块可以被放在组中的任何一个位置,所以不许检测该组中所有的块标记。
直接映射的cache可以看作一个简单的1路组相联;每个cache的一个项为一个块,形成只有一个块的组。有n项的全相联cache可以看作是一个n路的组相联cache,它只有一个组,组中有m个块,每一项可以放在该组中的任何一个块中)
提高关联度的优点是它可以提升cache的命中率,但是却增加了命中时间,也加大了比较器的开销。
备注:我们所说的块就是cache line
就是这样,命中率并不仅仅决定于缓存的容量,这是一个非常普遍的错误认知,cache相联度同样与处理器本身有一定关系。
终章 Pentium P4 与AMD Opteron的存储器
这一章大概就是CPU设计原理的最后一节了,为了做一个完美的谢幕,当然要用大家最关心的实际问题来结尾啦!
皓龙和奔腾在新的架构中肯定会对一个重要的细节进行修补,那就是我们这一章节详细讨论的cache,两者都附加有优化措施来提升命中率。首先是在缺失时先 返回被请求的数据,如先前小节中所描述的。二者都允许处理器在cache缺失期间,继续执行访问数据cache的指令,这一技术被称为无阻塞cache, 经常被工程师们用在乱序执行(看第二章去)的处理器上,来隐藏cache的缺失延迟。这种技术实现无阻塞有两个特点,第一就是指令与其他的工作来隐藏一部 分缺失延迟,第二就是重叠两个不同缺失的延迟。
要重叠多重cache缺失的大部分缺失时间需要较高带宽的存储系统来并行的处理多重缺失,事实上这一点也被用在GPU中(当任务一在处理时,因为需要寻找 数据所以会有延时,如果在任务一寻找数据的同时执行任务二,那么这一部分延时就会被隐藏掉,同样正因如此,GPU才被称之为高度并行处理器,另外,这种技 术也可以在CUDA编程中应用从而提高GPU的通用计算能力)。
奔4和皓龙的处理器都要预取指令(TLB的一种技术,有些深,没讲的一部分),也都采用内部包含的硬件预取机制来访问数据。他们观察数据缺失的模式,并利用此信息预测下一次取指令的信息,在循环访问时效果很好(循环指令)。
对于微处理器的设计者们面临的最严峻的挑战之一,就是支持像P4和皓龙这种每个周期可以执行多条存储器指令的处理器(超标量啊,别告诉我你忘了)。有两种 不同的技术可以支持一级cache中的多个请求。cache可以是多端口的,允许对同一cache块的多个访问同时进行。但是多端口cache一般非常昂 贵,因为多端口存储器中的RAM单元比单端口中的单元大得多(在具体说,单端口的块不能在同一时间进行多次操作,再多端口中增加了块的数量)。另外一种方 案是把cache分成段,并允许多个互相独立的存取操作对两个不同的段进行访问。
关于细节:AMD和Intel芯片的最大区别也许就是P4指令cache使用了踪迹cache,而AMD皓龙使用的是更加传统的指令cache。
不同于顺序的将指令装入一个cache块中以增加空间局部性,踪迹cache将找到的动态的包括所有分支的指令装入一个cache块。这样cache块中 包含的就是有CPU决定的执行指令的动态记录,而不是主存局部决定的静态指令序列。由于cache中带有分支预测,我们必须确认分支以及地址的有效性,以 保证有效的去指令。另外P4的cache中存的是为指令而不是皓龙的IA 32指令。
很明显,踪迹cache需要更为复杂的cache地址映射机制,因为地址已经不再是对齐的了。
然而踪迹cache可以提高cache块的利用率。例如,在传统的cache中,有分支转跳到另一个很大的块中,这样块的开始一部分占用的空间就可能不被 访问到。同样,由分支转跳可能跳出这样的块,那么块的最后一部分就很可能被浪费掉了。踪迹cache仅仅存储分支入口到离开记录之间的指令,这样避免了头 尾的浪费。当然这样也有一个弊端就是他可能在cache中多次存储相同的指令,条件分支的不同选择结构导致了相同的指令成为了多个踪迹的一部分,而每一部 分都会存储在cache中。
皓龙和奔腾在新的架构中肯定会对一个重要的细节进行修补,那就是我们这一章节详细讨论的cache,两者都附加有优化措施来提升命中率。首先是在缺失时先 返回被请求的数据,如先前小节中所描述的。二者都允许处理器在cache缺失期间,继续执行访问数据cache的指令,这一技术被称为无阻塞cache, 经常被工程师们用在乱序执行(看第二章去)的处理器上,来隐藏cache的缺失延迟。这种技术实现无阻塞有两个特点,第一就是指令与其他的工作来隐藏一部 分缺失延迟,第二就是重叠两个不同缺失的延迟。
要重叠多重cache缺失的大部分缺失时间需要较高带宽的存储系统来并行的处理多重缺失,事实上这一点也被用在GPU中(当任务一在处理时,因为需要寻找 数据所以会有延时,如果在任务一寻找数据的同时执行任务二,那么这一部分延时就会被隐藏掉,同样正因如此,GPU才被称之为高度并行处理器,另外,这种技 术也可以在CUDA编程中应用从而提高GPU的通用计算能力)。
奔4和皓龙的处理器都要预取指令(TLB的一种技术,有些深,没讲的一部分),也都采用内部包含的硬件预取机制来访问数据。他们观察数据缺失的模式,并利用此信息预测下一次取指令的信息,在循环访问时效果很好(循环指令)。
对于微处理器的设计者们面临的最严峻的挑战之一,就是支持像P4和皓龙这种每个周期可以执行多条存储器指令的处理器(超标量啊,别告诉我你忘了)。有两种 不同的技术可以支持一级cache中的多个请求。cache可以是多端口的,允许对同一cache块的多个访问同时进行。但是多端口cache一般非常昂 贵,因为多端口存储器中的RAM单元比单端口中的单元大得多(在具体说,单端口的块不能在同一时间进行多次操作,再多端口中增加了块的数量)。另外一种方 案是把cache分成段,并允许多个互相独立的存取操作对两个不同的段进行访问。
关于细节:AMD和Intel芯片的最大区别也许就是P4指令cache使用了踪迹cache,而AMD皓龙使用的是更加传统的指令cache。
不同于顺序的将指令装入一个cache块中以增加空间局部性,踪迹cache将找到的动态的包括所有分支的指令装入一个cache块。这样cache块中 包含的就是有CPU决定的执行指令的动态记录,而不是主存局部决定的静态指令序列。由于cache中带有分支预测,我们必须确认分支以及地址的有效性,以 保证有效的去指令。另外P4的cache中存的是为指令而不是皓龙的IA 32指令。
很明显,踪迹cache需要更为复杂的cache地址映射机制,因为地址已经不再是对齐的了。
然而踪迹cache可以提高cache块的利用率。例如,在传统的cache中,有分支转跳到另一个很大的块中,这样块的开始一部分占用的空间就可能不被 访问到。同样,由分支转跳可能跳出这样的块,那么块的最后一部分就很可能被浪费掉了。踪迹cache仅仅存储分支入口到离开记录之间的指令,这样避免了头 尾的浪费。当然这样也有一个弊端就是他可能在cache中多次存储相同的指令,条件分支的不同选择结构导致了相同的指令成为了多个踪迹的一部分,而每一部 分都会存储在cache中。
最后做个总结吧,其实不管是缓存也好,流水线也罢,无非都是通过减少执行时间来提高性能的,目前CPU的趋势会像GPU一样发展多核心,如何开发和利用多核心的优势,减少浪费,这正是未来工程师们研究的啊!