`
wezly
  • 浏览: 472243 次
  • 性别: Icon_minigender_1
  • 来自: 长沙
社区版块
存档分类
最新评论
阅读更多

序言

      也许很多朋友都会对CPU的原理比较感兴趣,从网上大家可以搜集到“利用晶体管的开关闭合进行二进制运算或数据存贮”之类的东西,但是CPU的性能由何决定?CPU内部的设计师如何的?分支预测,乱序执行到底是什么?超长流水线,RISC又是何物?为什么酷睿的CPU频率低,但是比高频率的P4性能好那么多?这些我们应用中遇见的问题绝对不是几句简单的话语可以解释的。
       写本文的目的,就是想让大家对CPU的了解更加深刻一些,更好的理解CPU到底是怎么运转的。但同样,考虑到仅仅是普及文,我个人不会对一些特别的技术和实现方式(比如逻辑电路的设计,编译原理等等)进行详细讲解,我们只需要知道这些东西通过一些设计就可以达到预期的效果,具体问题是工程师的问题。我会尽量让文章做到范围广,深度浅,把晦涩难懂的知识尽量具体化形象化,但不得不承认我也只是一个普通爱好者,所以错误也是难免的,希望大家共同交流,相互促进。

这玩意实在太长了,不可能一帖,甚至几帖就可以说完,所以我决定利用连载的方式来写这篇文章。

内容大体分为三大章

1 处理器的数据线路和控制实现

2 流水线及其带来的问题

3 存储器结构的讨论


系统学习是比较繁琐的,但是想要了解一些东西必须要把基础打好,希望有心的朋友仔细读下去,我相信也许你会爱上计算机。 
 
 
1.1  指令的执行过程(这里没有涉及到高级语言的编译,所以没有翻译指令)  
     
       CPU的性能由哪些因素决定?我想大多数人都会知道时钟周期这个概念,而另两条“指令数目”,“指令所需时钟周期数(CPI)”是并不为人所知的。而不同的内部架构,也便深刻的影响了CPI的大小,所以有些处理器频率低但是处理任务更快。不过别急,一口吃一个胖子是不行的,在我们对CPU的架构进行探讨之前,先来看看CPU是怎么处理一条指令的。
       一条指令,首先会根据程序计数器(记录当前运行的指令地址,本身也是一种寄存器)从内存中去处指令,通过指令字段的内容,选择读取一个或者两个寄存器(在汇编语言中,一条代码,即一个指令只能使用1或2个寄存器进行加减乘除运算,而寄存器,就是记录数据的一个东西,当然他还有更多用途,之后我们会提及)一旦取得寄存器的操作数后,就可以对指令进行定性,大致分为三种,存储访问,算术逻辑和分支(分支也可以叫做跳转,一般的高级语言都用if语句,判断一些特定条件从而进行两种或者更多的操作)存储访问指令需要对存储单元进行读出或者写入而访问寄存器;算术逻辑指令需要将ALU(运算器)计算得到的数据写回寄存器;而分支指令则会通过对数据的比较,决定是否对下条指令地址进行修改(也就是是否进行if语句中的or后面的语句)

话外音:一条高级语言首先会被编译为汇编语言,之后通过指令集将其编译为二进制信号,从而让CPU明白它需要做哪些操作。
              不要将缓存和寄存器的概念混淆,最好从百度百科上看看相应资料。缓存我们会在第三大章进行详细讨论。

1.2 数据通路的建立

什么叫数据通路?我想聪明的你一定知道,让一条指令顺畅执行下去,这就是一条数据通路,它包括指令,数据存储器,寄存器堆,运算单元和加法器(加法器是对程序计数器进行调节的,也就是说当前指令执行完毕之后,加法器对程序计数器加上4,就可以切换到下一个指令地址,关于为什么加4这里不多说,在第三大章我们会提及)。

想要构成数据通路,首先需要一个存储程序指令的地方,那么我们就需要一个存储单元来存储程序指令,并根据所给地址提供指令。当前需要执行的指令一定要放在一个存储单元中,这就是我们之前提到的程序计数器(PC),然后同一个加法器增加PC的值使它指向下一条指令的地址。

得到了指令地址,就可以让ALU对指令进行处理,从而得到结果,而ALU想要进行计算,就需要从寄存器获得数据并写入寄存器,这样,我们就需要一个寄存器堆,来暂时存储这些指令地址,信息等等。

如果是储存访问指令和算术指令的话,那么接下来只需要将寄存器的数据显示,或者暂时储存就可以了,若是分支指令,就需要对ALU的数据和寄存器的数据进行比较,如果为真,则执行if语句后紧跟的语句,如果为假,就会执行if语句中or后紧跟的语句,所以说分支指令总是延迟的,因为在没有得出指令的真假时我们不能对接下来的指令进行处理。 
 
 
1.3 指令的处理实现

一条指令,通过拆分(解码器就是对指令进行拆分,重排等操作的,使用逻辑电路从而达到了一种特殊算法),因为ALU的处理能力是一定的,所以有些复杂指令不能在一个周期内处理完,而需要拆分或者让ALU利用更长的一个周期重复处理这段指令。
对于单周期实现,和1.2节中的数据通路非常相近,如果有不熟悉的朋友可以回阅上一节。

多周期的数据通路,与单周期通路最本质的区别有三点:
1 指令和数据使用相同的储存单元
2 只有一个ALU(也可以使多个,但目前不对其进行讨论),没有了加法器
3 每个重要的功能单元都加上了一些寄存器存储输出值,使后面的时钟周期得到需要的信息(这是寄存器的另一个用处)。

当一个时钟周期结束后,我们只需要将处理得到的结果反馈给PC程序计数器,那么它就可以再次发射指令,所以就没有必要安置加法器。

说了这么多,相信有心的朋友们就会发现,其实我们所说的单周期指令实现,就是早期的CISC复杂指令集计算机,而多周期就是RISC精简指令集计算机。而 CISC这个东东有两个致命缺点,第一,它不能够很好的进行流水操作(第二大章的内容,以后详细讲解),第二,因为指令的复杂程度不一,那么如果为了性能而使用可变时钟周期的设计,会大大增加控制器等其他单元的设计问题,而若是使用固定的时钟频率,又会造成极大的浪费,所以CISC现在已经基本被抛弃了。


1.4 异常

在CPU设计中,最具有挑战性的一个问题,是一个程序异常(打断程序运行,比如错误的保存了指令的结果),被中断(来自处理器外的异常,比如内存的读写错误),就比如算术溢出。很多业内人士并不区分开两者,都称之为中断。

那这些异常是如何处理的呢?为了对其进行处理,我们必须知道是那些指令引起的异常问题,目前有两种方法,第一种需要一个状态寄存器,其中有一个字段用于记录异常产生的原因。另一种方法利用向量中断(用来控制转换的终端地址)这两种方式在这里我们不去展开细讲,更多的是为了下面几章做一点点铺垫。


1.5 实例  奔腾处理器的内部架构

奔腾系列都是采用流水线设计,它让多条指令重叠从而达到更高的指令吞吐率,其时钟周期的长度有单个功能单元的延迟决定。这里我们暂且放一放,之后还会有详解。
Intel的IA 32指令集(指CPU能识别什么类型的指令)非常复杂,是对其实现控制电路的一大难点,虽然核心内容都是源于前几节的内容,但是想要出成品并不容易

IA 32的指令,有些可能用到几十个周期,甚至超过几百个周期的指令。例如,串行传送指令要求计算并修改两个不同的存储地址,并且存储一个字节串。而且他复杂的寻址模式也使其使其结构实现难度大大提高。

而Intel的工程师巧妙地运用多周期数据通路和微程序控制器(使用代码而不是01来表示控制的方法),这样,即使是需要周期数不同的指令,也会减少更多的周期损失(因为频率是固定的)

1.6 奔腾4的结构

超标量,这是在奔腾4系列CPU中引入的一种技术,简单来看,这种技术使得处理器可以有多条数据通路,每一条处理某一类型指令:存取指令,ALU计算,分支。这样处理器便可以在一个周期内执行多条指令(当然这些都仅仅是皮毛,之后我们依旧会在第二大章进行讨论)。而所谓的微操作,便是利用某种技术,让每一条指令分配给不同的数据通路,从而达到更高的效率。

在奔腾4中,踪迹缓存技术就是来存储微指令的,这是一种解决方案,记录微指令将会被引入哪一条特定的数据通路。这种缓存技术比较复杂,我们会在第三大章中稍作了解。

奔腾4使用简单的硬联线控制和简单数据通路,结合踪迹缓存,获得了令人吃惊的时钟频率,当然还要得益于深度流水线的引入,不过这都是后话了。

第一章结束


 10月2日


第二章 流水线,更高更快

2.1 流水线初涉

相信对于大多数了解硬件的朋友,流水线早已不是什么陌生的词汇了,它并不难理解。

我们先回顾一下一般的数据通路如何处理指令,首先PC会将指令地址送给ALU,ALU进行处理将数据存储,然后PC值加4 ,这样就可以进行下一个指令的处理。而流水线便是不间断的发射指令,原本一个指令需要得到结果才会被处理,而现在则是让指令充斥着整个数据通路,在ALU 处理第一条指令时,第二条指令已经进行取值,当第一条指令处理后,第二条指令立刻被送往ALU,这样就减少了非常多的时钟周期(一个指令需要经过取指令,访问寄存器,ALU操作,访问数据,访问寄存器这五步)(再次进行解释,第二步中的访问寄存器是让ALU得到操作数据,第四步访问数据也就是得到ALU的计算结果所必须的一些数据,例如a+b,我们需要知道a和b的值是多少。而访问寄存器就是对PC的操作)如果这样说还不明白的话,我们简单打个比方,我们做饭,需要先把菜做好,然后去煮米饭,而流水线话操作时,我们可以一边煮米饭,一边做菜,这样就减少了整个做饭的时间。而假设煮米饭要10分钟,做菜要 20分钟,那么整个流水化操作后就需要20分钟,也就是说流水线操作的时间取决于需要时间最长的事情,在CPU中就是任务的执行周期数取决于最复杂,处理速度最慢的指令(比如一个超长浮点运算)

附言:流水线增加的是CPU指令的吞吐率,而不是减少了单个指令执行的时间,而且因为某些特殊的问题,还会让单个指令执行时间增加,例如流水线寄存器的引入

 

2.2 流水线结构的冒险

我想当看完2.1节后大家一定会说,流水线竟然如此简单?确实流水线并不难理解,但是真正实现过程中人们发现了相当多的问题,会引起流水线的处理停顿,我们称为冒险。

结构冒险
这是第一种冒险,即硬件不支持多条指令在同一个时钟周期内执行。比如我们做菜的时候,因为家里的电器因为线路的原因只能工作一个,那么我们只能先煮米饭,再做菜了。
如果用CPU角度来考虑的话,如果在流水线中一个指令在该周期内需要从内存中取得数据,而同时另一条指令需要写入内存,这样,因为DDR内存在一个周期内只能执行一次读或者写操作(数据总线只有一条,而对于GDDR3之后的显示卡内存,因为具备多条数据总线,可以对内存同时进行读写操作)那么这两条指令就造成了结构冒险。

数据冒险
这是第二种冒险,在一个操作必须等待另一操作完成后才能进行时,那么就会造成流水线停顿。例如我们需要知道a和b,需要将两个数相加得到c的值,之后再对 c进行立即数操作(直接对数值进行加减乘除运算,而不需要读取被加数的寄存器地址),那么必须等a和b相加后得到的数据写到寄存器中才可以进行下一个操作。

控制冒险

第三种冒险,当处理器需要根据一条指令的结果作出决策,此时其他的指令可能在执行中。好比一个分支操作,我们需要知道是执行分支中哪一条指令才能进行下一步操作,这样接下来的指令就需要得到分支的结果才能进行处理。 
 


2.2 有问题就要解决

既然流水线有那么多限制,为何不去简化它呢?或者说直接不流水。可是经过很多科学家的验证,流水线对程序提升的性能即使存在如此多的冒险,依旧客观,根据流水线加速比的公式,一个5步骤处理的CPU可以提升3~4倍的性能(实则5倍,但是因为有冒险)

不去解决流水线的冒险可以吗?对于像GPU这种高度并行,数据之间关联度低(也就是流水线的冒险度较低,不易发生流水线停顿)我们完全没有必要去理会,不过通过对指令并行的提升(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,而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,后半段则是传送数据给下一个阶段。


附录:superpipeline 超长流水线。
超长流水线,也称深度流水线,它是一种高级流水线技术,在这里我们简单来探讨一下。

首先要指出的是,超长流水线并不是指ALU更多的流水线。它的含义是将一个较短阶段(stage)的流水线划分成更多阶段。
其实在我一开始接触的时候也不明白,为什么划分更多阶段能给予良好的流水性能呢?其实这一点不难理解,因为这样,在一个周期内流水线内部的指令数会比原来更多,同样因为划分的更加细,频率也更容易提升(因为流水线的频率取决于速度最慢的那一阶段),这样,在计算大量数据时,这种超长流水线就可以得到很好的加速效果,但是,若是出现冒险,那么损失的时钟周期是非常可怕的。P4当中引入了大量的缓存(cache),目的就是让指令所需的数据能在cache中得到而不去等待慢吞吞的内存(目前内存的频率都是200,DDR运用了DIMM上下沿并发和数据预取的技术从而等效达到了高频率,但是一个1.8G的CPU 依旧需要9个周期去等待内存数据)P4能达到较高的频率,也很大程度上依靠了这种高级流水线技术。 国庆最后一天特别专题(今后2周上课所以无法更新= =)
高级话题:性能的再次提升

流水线的开发,同样造就了一个概念,叫做指令集并行,。主要通过两种方式提高指令集的并行能力一个是增加流水线技术,另一种则是设计更多的内部元件从而在一个流水线中的每级发射更多的指令。前者我们已经有所探讨,他就是P4中引入的superpipeline。后者就包括超标量设计,涉及到了流水线的宽度。

我们现在深入探讨第二个话题。这种多发射技术,目前有两种比较聪明的实现方法,他们的区别就是编译器和硬件之间如何分工,由于分工主要在于某些决定是在编译期决定的还是在运行期决定的,所以前者称之为静态多发射,后者叫做动态多发射。

静态多发射处理器利用编译期从而对指令进行打包和处理各种特殊的流水线冒险,在静态多发射中,我们可以将多条指令看作一条超长指令,复杂指令。同样,因为本身可以看作一条超长指令,那么它便被固定了长度,操作数。这就是所谓的VLIW,超长指令字,相信对R600了解的朋友一定不会陌生。

动态多发射处理器也被称之为超标量处理器,在比较简单的模型中,指令是按照顺序发射,每个周期可以发射1条或多条指令,或者说不发射。非常明显的是,这种处理器要达到较好的性能非常依靠编译期对各条指令的调节,错开指令之间的依赖关系从而达到更高的发射速率。所以目前超标量处理器会更多的在指令排序方面下工夫。

附录:多发射处理器的数据通路
非常简单,我们只需要在一般的多周期数据通路上增加更多的执行单元,比如ALU,对各种单元进行划分,比如A单元执行浮点,B单元执行整数,通过对寄存器的大小调节,便可以适应不同的指令。


第二章,完 
 第三章 层次结构存储器

1 导论

假如你是在做高数作业,为了快速计算,你会把积分表放在最显眼的地方,其次为了防止对一些积分变换的不熟练,你会放一个例题本在一旁,这样又不会的变换就可以直接查。而如果有一个题你怎么也做不出来,就值得去求教老师。

其实本来作业应该是我们自己做的,实在不会就去请教老师,但是借助了积分表和积分变换的例题就能更快的解决题目,而不需要去请教老师,因为请教老师太麻烦太慢了。这样,在CPU当中也存在这种类似的环境,也就是说最能帮助你解决当前问题(指令最需要的数据)的应该优先拜访(优先访问),再或者说你有两本词典,一本是英语5000实用词,另一本是牛津高阶英汉词典,查单词的时候,我们当然会先去查看实用词词典咯,大辞典查起来太慢了。

下面就得引入必备的理论知识了

时间局部性:如果一个数据项被引用,那在不久之后它很有可能再次使用。比如在第一个例子中,比如一个循环指令,每次循环都需要一个常量B,那么B就非常容易被调用

空间局部性:如果某个数据项被引用,那么不久之后与它地址相近的数据项可能会被引用,也就是说数据间有相依性啦

层次结构存储器,也就是按照这两种原理所衍生的。现在的计算机就有缓存和内存之分(寄存器不属于层次结构存储器,它的目的是存储目前需要使用的数据,而不是可能会使用到的数据,切记切记),缓存小而快速,一般都在CPU的uncore部分,内存就很容易理解了,在缓存和硬盘之间的,速度和容量在两者之间其缓冲的东东。为什么速度快容量就要小呢?这是一个逻辑电路的知识,简单的来说就是为了得到更高的速度,那信号的发射频率必须提升,而为了得到更大的容量,那么就必须保证信号传输中减少干扰。如果频率提高,干扰就会增强,所以两者比较难以统一。


存储器的层次结构可以由多级组成,但是数据复制每次必须在两个相邻层次间才可以进行,一个层次结构中存储信息的最小单元,我们称之为快或行(line,cache line,眼熟吧)如果处理器需要的数据出现在高层(速度快,容量小的cache)的块中,测称之为一次命中,而命中率就是指我们需要的数据在cache 中所占的比例。缺失率也就是cache中没有的数据占需求数据的百分比。
.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,根据局部性原理。


这时候追求细节的朋友们可能会提问,CPU怎么知道cache中有没有所需的数据呢?如果在,又是如何找到的呢?最简单的方法就是给内存的每一个数据(字)分配一个缓存的地址,每一个内存的数据地址对应一个确定的缓存地址(数据重复),也就是说,当我们读取内存发现cache中含有该数据那么就会去 cache中寻找,从而缩短的很多时间。这种简单的分配方式叫做直接映像。由于每个缓存的地址可对应的内存中多个不同地址,怎么才能知道所请求的数据是否在缓存中呢?这就得引入标记这个概念了,标记必须包含能够判断缓存中的数据是否为所请求数据的地址信息。我们还需要一种办法来判断缓存中的确没有有效的信息,我们就必须知道缓存中部分标记要被忽略,最常用的方法就是有效位,说明一个cache块中是否有有效地址。如果这个位置没有设置,就不能读取其内容。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics