虽说上了半学期的体系结构的课程.但是似乎只听老师讲,总是无法满足我很多的问题.并且总觉得学了没用.但上网查了查,发现体系结构还是蛮重要的,关键在于CPU执行程序的过程.几乎所有机器相关的优化都是靠CPU的相关知识.所以还是有必要多写一篇来记录一下所学的知识.
到底CPU是怎么工作的?
从物理角度来说的话,CPU会把二进制代码,转化为它引脚上的高低电平的变化.通过管脚的高低电平,控制外设.一般外设已经是设置好了了,发来什么电平顺序就执行什么内容.比如鼠标,键盘,内存,显示器,显卡什么的.程序员只用了解如何发送和接受其二进制电平就行.
从程序的角度来看.简单来说,CPU从cache里面取数据.然后解析二进制代码中相应位.然后通过位的不同组合,对应执行不同的功能.
CPU如何工作总结起来就这么简单~但往细了说就有很多难点了~
下面再来详谈具体CPU是如何执行的.
首先你得知道流水线技术,我就不多说了.这是并行执行程序的必要手段.现在所有CPU都是在这个基础上进行的.
CPU如何操纵寄存器?
之前我一直以为每个寄存器是分开放的一个元件,结果看了书才发现是集中在一起的.已经以元件的方式封装好了.并且通过类似译码电路的输入输出,来控制.这个元件叫寄存器堆(好像还叫寄存器文件).
CPU是如何跟内存交互的?
实际上CPU永远只跟cache交互.如果cache没有命中,那么就会停止,并让MMU来把内存换入到cache中.只要操作系统保证了页表的相关机制,那么CPU能识别的都是逻辑地址.具体可以看我的另外一篇文章,写程序如何运行的虚拟存储器部分.
流水线是如何处理中断的?
这一点是我之前的疑问.那就是中断是在什么时候开始确认要执行的?实际上对于现代CPU中为了保证每个流水线阶段保持独立,在每个阶段之后都会有一个状态寄存器来保留每个阶段上的程序状态.当前指令执行玩还会将其状态附送给后面执行的程序.所以当产生了中断的时候,中断硬件直接把状态寄存器的中断位设置,然后硬件就知道来了中断.到了执行阶段,通过一定的手段,保存上下文,然后更新PC寄存器.从而达到中断的效果.其实现代处理器要复杂得多,对于带了乱序引擎什么的CPU来说,这一点可能比我说的复杂得多,但是主旨就是用多个状态寄存器来维持当前程序的状态.这是我看CSAPP后自己的想法,毕竟真正怎么工作的只有芯片厂才知道.
流水线结构会带来哪些麻烦?
这个各大教材上都会说明,我也简单说下.
结构冒险: 缺少硬件,导致无法并行
数据冒险: 由于指令之间的数据有依赖关系,导致并行失败.
控制冒险: 由于程序跳转所引发的并行指令失效.
实际上所有非算法的底层优化都是根据这三种冒险来优化.
结构冒险优化:
对于结构冒险,这个绝大部分书都不怎么讲.不过还是可以提一下.自己写代码的时候注意一下.一般对于除法,尽量少用或着间隔这用.如果你的代码里有两个独立除法分隔开了,你可以把中间插入一个乘法这样子.当然这个乘法是你代码中需要的才行.
数据冒险优化:
这个是我看计算机体系结构:量化研究方法(第三版)的.提到了很多教材上面没说的东西.不过硬件的发展也快,学校又没有买新书...所以我着重介绍软件方面可以利用起来的东西.
教材上说的有三种优化方法:插入气泡,插入nop指令,使用旁路技术.
实际上前两个真的是用的很少,并且我们老师把重点放在前两个,而关键的旁路技术只是带过...估计还是学校渣了吧....好吧还是说正题.
实际上数据冒险分为两种:
1.名字相关
这个估计很多人都没听过吧.其实就是你对寄存器太节省了.好比说你R1一直用来存放数据,但是这个数据只用了一次,之后R1就给其他过程用了.这就可能造成R1使用冲突.也就是所谓的名字相关冲突了.这个可以通过编译器实现,也可以通过硬件动态预测实现.对于我们写高级语言的人来说,用处不大.
2.数据冲突
这就是重点了.因为下一步的计算需要这一步的结果,所以导致的冲突.还可以细分为3种:
1) 读写冲突 读在写之前
2) 写写冲突
3) 读写冲突 写在读之前
这几种情况可能逻辑上说不通,但因为CPU内部指令发射后,执行的顺序不一定是顺序的,很可能被乱序引擎重新调整顺序执行.如果没有乱序引擎的话就无所谓了.
什么是动态调度?
就是由硬件动态调整指令执行顺序以减少停顿的影响.能够处理一些在编译阶段无法知道的相关关系,并简化编译器设计.
当然,这个硬件一听就很复杂,至少用硬件实现一个算法一定是很不容易的.并且还会造成一些额外的麻烦.好比这个动态调度就对异常(中断)处理比较困难.很可能会产生不精确异常.不精确异常的意思就是运行结果与顺序执行结果不一致.
CPU在这时候到底如何运行呢?
首先就是译码(也叫发射),然后进入分支预测(译码过程即可预测),再然后进入乱序引擎(即读操作数之类),最后有个退役单元.(好像是奔4的芯片的过程,这个我参考的linux汇编书上的)分支预测是用来解决控制冒险的,乱序引擎就是我接下来要说的.最后的退役单元则是用来监视指令执行阶段的,保证指令执行的结果是顺序的.
这里的对应关系不同书上不一样,上面的过程基本算是我的理解,如果有错误,还望指正.
顺带一提,发射的过程也分为两种:静态多发射---超长指令字(多指令打包执行),动态多发射---超标量(单指令分开执行,流水线).其实也就是对编译器干的活的一个补充.
为什么编译器可以优化,还需要超标量处理器?
1.并不是所有阻塞都是可以事先知道的.比如说cache的缺失.
2.如果处理器采用动态分支预测推测分支的结果,那么由于这些信息依赖于预测和分支指令的真是执行情况.编译器就肯定无法知道了.
3.由于不同CPU流水线延时和发射宽度根据处理器的具体实现的不同而有很大的差别,所以最佳的编译代码并不固定!
乱序引擎是如何工作的呢?
实际上译码工作还做了一件事,就是检测资源冲突的情况.在乱序引擎中则等待数据不存在冲突之后才继续执行的.其中乱序引擎运用到了Tomasulo的算法.这个算法简单来说就是跟踪指令操作数就绪的时间,减少写写和写读的冲突.具体的算法如何调用其运行我就不写了.毕竟硬件更新太快了.
控制冒险:
其实也就是分支预测.毕竟如果流水线不判断分支的话,会造成较大的流水线资源浪费.
分支预测有哪些?
1.假设法.固定选择无分支的状态,当然这种预测很差劲.
2.缩短分支的延迟.主要在两方面,计算分支地址,判断分支条件(判断不是预测).
3.动态预测分支.通过历史信息来预测.理想情况下,准确度与分支被执行的频度相符.
最新的这个预测硬件叫做竞赛预测器(另外一本2012年的书).简单来说就是采用多个预测器同时工作,并且选一个预测效果最好的来预测.其实这种针对历史的预测更多的指循环来说的.如果是循环的话,至少有2次预测不成功吧.但是中间的多次循环则是正确的.当然,循环中有多个if条件分支也是可以预测的.不过这也就提醒我们了,尽量让内循环的循环次数多一些.
4.预读
简单来说就是把分支的两种情况都先预读,然后根据判断来选择一个分支执行.
我这写的还算简单的,总的来说还有很多技术没有说到.但是对于了解CPU是如何运行的大体框架基本已经写到了.
发射(组合--超长指令字,并行---超标量)----分支预测(动态--处理器与静态---编译器)-----乱序引擎(指令重排序)-----退役单元(确保指令顺序执行).
这些书上都没有指明是否为多核.不过我个人认为对于多核基本来说过程差不多,可能对内存的并行操作复杂些。.用指令控制用哪个处理器运行,肯能会用退役单元来安排.具体的中文文章太少,搜出来的也只是在扯些表面文章.有时间看完我会来补充的.
还有一点就是关于发射槽和流水线的矛盾问题.既然发射槽是多个指令并行发射的,那么流水线似乎就不是教材上的阶梯式流水线,而是类似长方形的流水线.流水线在部分阶段,指令顺序是乱的?最后到退役单元才是正常的阶梯式流水?首先这一点是有其适用范围的,也就是说一部分情况下两条指令可以在同一个处理器中完全并行的执行.其实也就是定点指令和浮点指令可以并行执行,因为浮点运算单元是单独一个模块,所以才可以这样.如果只是定点指令,那么就是阶梯式的执行指令.当然其实CPU为了很其他一些指令也能并行的执行,包括内存操作和分支操作按书上来看也是有单独的器件的.
写的比较混乱,主要是按自己的疑问来写的....所以没太考虑结构....
如果之前哪个位置有错误,还请指教~
PS:
不要以为CPU的底层知识就不用了解~你看下面这个问题,虽然问题是c++和java的代码效率,但是根本原因是预测分支的问题~
http://stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-an-unsorted-array?lq=1