作者简介
甄建勇,高级架构师(某国际大厂),十年以上半导体从业经验。主要研究领域:CPU/GPU/NPU架构与微架构设计。感兴趣领域:经济学、心理学、哲学。
我们很多人都有下面的经历:
下班回家后,吃饭时,碗筷都已准备好,在吃第一口饭之前,顺手点了一下旁边的ipad或者手机(以下统称为计算机),想继续追昨天没看完的电视剧。
你可知道,你这“顺手一点”的背后,计算机内部都发生了哪些神奇的事,才让你看到新的剧情,新的画面吗?
今天,我们就虚拟一个小人儿,名叫小土孩儿,她将顺着你的指尖滑下,进入到计算机内部,看看会遇到什么你可能知道,也可能不知道的事情……
毫无疑问,小土孩儿离开你指尖的第一站,就是屏幕的外层,其实从技术角度看,屏幕的外层就是触摸屏,其本质和电脑的键盘并无二致,作用都是捕捉用户操作。
对于矩阵键盘,其内部有扫描电路,会隔一段时间扫描一下,根据电平的高低,来判断是否有按键被按下。当小土孩儿掉落到键盘上时,会对按键(假设是空格键)有个压力,这个压力使空格键下面的电路导通,这样键盘的扫描电路在下次扫描时就会发现这一情况。对于一次单击操作,我们从宏观上认为我们按了一次键,实际上,键盘扫描电路会有防抖机制,即在一段持续的时间段内,某个按键一直没按下,才算一次单击。如果按的的速度太快,防抖逻辑可能会认为是误操作。如果按的时间太久,可能会被当成是“双击”,或者是“长按”。我们可以根据人类通常情况下的操作速度来设计合理的扫描间隔。
按完空格键之后,键盘控制芯片将空格键对应的编码保存在一个寄存器中,并拉低与处理器(CPU)相连的一条线,即向处理器发送一个外部中断信号。
CPU内部的中断控制器收到这个外部中断信号之后,会把CPU内部的一个控制寄存器“置1”(后面会提到),表示收到了一个外部中断。在中断控制器内部还有另外一个控制寄存器,表示哪些中断要被屏蔽,或者哪些中断需要CPU进行处理。
经过屏蔽处理的这个中断,会附着在CPU内部正在执行的一条指令上,表示这条指令执行时发生了中断异常。
我们知道现代CPU中,指令处理流水线一般是多级流水,所以我们要找一个合适的中断异常附着的时间点,又因为CPU中有乱序执行技术,所以我们要在指令顺序被打乱之前附着,所以,这个附着点一般是指令译码阶段。
这条被异常附着的指令会随着CPU流水线,从译码阶段开始,依次向下一阶段传递。传递过程中,被异常附着的指令不会被发送到执行单元。比如,被附着的是一条ADD指令,这条ADD指令正常状态下会被发往加法器执行加法操作。
当这条指令来到ROB(Re-Order-Buffer)时,CPU将开始异常处理。
CPU内部,一般包括分支预测->取指->初级译码->保留站(reservationstation)->乱序发射->再次译码->指令执行->RoB等流水级。
程序中一般包含大量的分支指令,而分支指令的后面要执行的指令是什么,依赖于分支指令的执行结果。而知道分支指令的结果,要在指令指令阶段才可以。这时我们面临两个选择:
1. 等到知道分支指令的结果之后再去读取分支指令后面的指令。即,“不见兔子不撒鹰”
2. 可以猜一个分支指令执行结果,根据猜的结果,提前读取分支指令后面的指令。即,“投机执行”。
很显然,不见兔子不撒鹰式的处理方式,会造成分支指令前面的流水级出现空泡(bubble),也就意味着CPU性能下降。
而投机执行的后果是,一旦猜错了,必须引入猜错处理逻辑。所以,为了提高猜中的概率,CPU引入了分支预测机制。
基本的分支预测算法很简单,就是增加一个饱和计数器来猜测分支结果。比如,一个2bit的饱和计数器,来预测if判断分支指令的结果,其中0、1表示强跳转与弱跳转,2、3表示强不跳转,弱不跳转。其工作机制如下:
最开始,设置这个counter为1,表示弱跳转。
如果猜中,counter减1,变成跳转。如果没有猜中,counter加1,变成2,表示弱不跳转。
当counter为0之后,仍然猜中,counter将保持0,即出现饱和之后,持续猜中时,counter值不变。
以上机制,简单直接,但是有个很大的缺陷,就是可能会在1、2之间颠簸,使猜中率为0%。为了解决这个问题,CPU的分支预测机制引入了其它更复杂的方法。
就是根据PC(program counter)的值,将对应地址的指令读到CPU内部。而指令一般都存放在外部存储器中,道阻且长。为了提高取值速度,CPU一般会引入inst_cache和MMU。关于cache和MMU,我们之前有聊到,请参考“甄建勇_五分钟搞定”系列文章。
读取上来的指令,会被送往译码单元。即,指令的识别。对于RISC指令集的CPU来说,译码器比较简单。而对于CISC指令集,译码就变得很麻烦,因为同一条指令在译码时,指令后面的内容会依赖于指令前面译码的结果。比如intel的x86处理器的译码单元,为了提高译码速度,只能“全面撒网,重点培养”,即,同时译码所有的可能,然后最后根据一部分译码结果,来选择其中那个正确的。
此外,实际CPU中,最前端的译码单元,只需译码指令的一部分内容就可以指令的去向,所以没必要在最开始就全部译码。比如,只要区分出指令的类型,就可以发往下一阶段。指令后续的译码由对应的执行单元翻译即可。
在保留站之前的流水级,就像一条窄窄的巷子,为了保持指令顺序,指令在巷子里排着队,慢慢的前行。我们知道,巷子里的指令,有些指令之间是有依赖关系的,而有些是没有依赖关系的。
保留站就像巷子尽头的一个大广场,让那些本来排在后面,却和前面没有依赖的指令先行执行,即指令的超车。
对于WAW依赖(如下指令序列中的1和3),纯粹是因为寄存器数量不够引起的依赖,我们完全可以通过增加寄存器数量来解除指令之间的依赖,让两条执行流同时执行。就像我们去饭馆吃饭,结果发现需要排队,而排队的原因竟然是因为饭馆的筷子只有1双。
有依赖 | 无依赖 | 重命名 | |
---|---|---|---|
1 | ADD R3, R1,R2 | ADD R3, R1,R2 | |
2 | STORE addr0, R3 | STORE addr0, R3 | |
3 | ADD R3, R4,R5 | ADD R60, R4,R5 | R3 -> R60 |
4 | STORE addr0, R3 | STORE addr0, R60 |
经过寄存器重命名处理的指令,会呆在保留站内随时待命,一旦指令所需的操作数全部准备好之后,就会被发射到相应的执行单元,很显然,这里的发射可以是乱序的,同时发射的指令数量也可能超过一条。即,乱序执行和多发射技术。
关于指令的执行,就是“八仙过海”了。不同的指令,执行过程差异很大。我们之前聊过“甄建勇_五分钟搞不定_1+1=?”系列,介绍了计算机中加法器、乘法器具体的实现细节,感兴趣的同学请参考。
“出来混,迟早要还的”,ROB就是我们要还前面欠下的“乱序”的帐。在指令进入保留站的时候,我们需要登记指令的先后顺序,等乱序发射并执行的指令完成后,将进入到ROB,也就是另外“一个广场”。为了保持程序原本的正确顺序,我们需要按照当时进入保留站的顺序,依次将ROB中的指令依次移除,即指令的提交。
而最开始提到的被附着异常的指令,也是在这里处理的。ROB的另外一个原因是为了给操作系统一个“精确异常”, 即,处理异常前要把异常指令前面的指令都执行完, 后面的指令都取消掉。
上面提到,ROB在收到附着异常的指令之后,会进行一系列的操作:
首先就是给CPU的其它模块发送指令取消信号,将异常之后的所有的指令取消掉。其次要保存被附着指令对应的PC值。此外,还要修改CPU内部的控制寄存器,将CPU设置成内核态(CPU一般都有多个状态,比如,内核态、用户态、debug态等),最后,设置PC值为异常向量的入口地址,然后从入口地址取值,开始执行入口地址处的指令。
异常处理是一个复杂的过程,需要软硬件协同完成,上面提到的是硬件的异常处理,当CPU开始从异常处理入口取值执行后,执行的就是OS事先设定好的操作。异常入口处的指令一般是跳转指令,不同的异常入口处都有相应的跳转指令,这些跳转指令紧密的挨在一起,就像一个向量一样,故称“异常向量”。
异常向量是OS的一部分。所以当CPU执行异常向量时,就开始执行OS的代码了。OS异常处理过程一般是先保存处理器现场,然后读取CPU内部的控制寄存器,就是前面提到的那个控制寄存器。读回来,发现是外部中断引起的异常,OS就继续读取外部中断控制器的寄存器,同时将中断清除。读回来发现是键盘有人按下了,就继续读取键盘控制器的寄存器,发现被按下的是空格键。
OS接下来要查找这个空格键要发给谁,即哪个进程需要这个空格键。经OS查询发现,是一个叫X奇艺的视频软件在等待按键,于是就将空格键值发给X奇艺,并唤醒X奇艺进程。
X奇艺被唤醒之后,发现OS发来的是一个空格键的键值,假设X奇艺程序的事先设定是,当处于暂停播放状态下,按一个空格,表示视频继续播放。
接下来,X奇艺就会从网络或者本地磁盘读取即将显示的数据,调用视频解码器的驱动程序,驱动程序将设置解码器的寄存器,并是能解码器开始工作。当然,如果即将显示的内容可能是一些矢量数据,需要GPU参与,根据矢量数据进行渲染。
GPU最重要的功能是通过给定虚拟相机、3D场景物体以及光源等场景要素来产生(渲染)出一幅2D的图像,而GPU渲染一幅图像,一般包括多个阶段,分别是:顶点数据的输入、顶点着色、曲面细分、几何着色、图元组装、裁剪剔除、光栅化、像素着色以及测试与混合。
顶点数据(Vertex)一般包括顶点坐标、法线、颜色以及纹理坐标等信息。
顶点着色器(Shader)收到顶点数据之后,主要是进行坐标变换,即,将局部坐标变化到世界坐标、观察坐标。
曲面细分(Tessellation)将输入的较大的三角形切分成更小的三角形,使得离摄像机近的物体细节更丰富,而离摄像机远的物体则细节较少。
几何着色(Geometry)将输入的独立的点、线、等基础图元(primitive),扩展成多边形。
图元组装(Primitive Setup)阶段主要是将原始的图元按照规则组装成指定的图元。比如裁剪(Clipping)窗口以外的图元、剔除背面(Culling)等看不到的图元,以减少后面阶段的运算量。
光栅化(Rasterization)主要是将3D连续的物体转化成离散的屏幕像素点。
像素着色会根据光照等因素,决定每一个像素的最终颜色,也会进行阴影的处理,我们看到的一些酷炫的效果主要是像素着色阶段产生的。
GPU渲染的最后一个阶段是测试,包括裁剪测试、模板测试、深度测试等。只有通过测试的像素才会进行混合,比如Alpha混合。Alpha表示的是物体的不透明度,Alpha=1表示完全不透明。
经过以上长长的渲染管线(Rendering Pipeline),GPU就完成了一幅图像的渲染,并将渲染好的图像数据写到显示控制器可以读取的帧缓冲里。 图像的显示
图像以及图像的显示是个复杂的过程。光图像的格式就多如牛毛,比如CIE的XYZ、LUV、LAB。RGB的RGB、sRGB、AdobeRGB、scRGB、DCI-P3、Rec.709、ACES等。Luma+Chroma的YUV、YIQ、YpbPr、YCbCr、xvYCC、IPT、ICtCp等。Hue+Saturation的HSV、HSL。以及在打印机领域广泛使用的CMYK。每种格式都有对应的colorspace,下图就是BT的两个标准对应的colorspace。
定义这么多格式的目的都是为了跨越让显示器显示的图像和真实世界中的物体,在人眼看来是一样的,而显示器本身也在不断进化中。
下一步,我们的显示器可能是这样的:
其实,在图像格式和显示器之间还需要定义数据传输的协议,比如HDMI,同理CPU与GPU之间也需要数据传输的协议,比如PCIe。
显示器的控制器从帧缓冲里读出GPU渲染好的图像数据,通过和显示器连接的总线,传到显示器内部的控制器,并最终控制显示电路,将图像显示在屏幕上。