计算机上运行的程序种类繁多,有的程序代码包含大量分支(例如编译器、人工智能、电路真值表生成),需要CPU拥有强大的分支预测器,有的程序代码频繁取用数组元素(例如矩阵计算),对CPU的存储访问能力提出了很高的要求。设计一个万能型的CPU,要求它运行所有种类的代码都能达到快速度,这是极其困难的,但是如果能够区分程序类型,针对性地设计多种处理单元,令其分工合作,则有可能满足所有需求,这被称为异构计算。
PA-8700微处理器,蓝色大块是数据缓存,绿色和橙色大块是指令缓存及其标记位,计算单元龟缩在右上角
在讲述GPGPU的运算优势之前,我们先来看看初中数学水平的简单向量加法。考虑两个宽度为16 的向量相加,{1,2,3,…,15,16}+{1,2,3,…,15,16}={2,4,6,…,30,32},不难发现,其中每组对应数字的相加,相互之间并不干扰,不存在相关性,而且没有分支,可以完美地实现并行。只需让一条加法指令同时控制十六条逻辑通路,引导十六组寄存器中的数据进入ALU,再同时写回寄存器,就可以并行做完这个宽度为16的向量加法,用体系结构的术语来说,这被称为宽度为16的SIMD执行(单指令多数据)。
研究显示,与此例类似的向量计算代码经常出现在某些特定种类的程序中(例如图像编解码)。因此,这种能够一次处理多组数据的SIMD处理单元就可以考虑作为GPGPU的基本构建模块。以NVIDIAFermi架构的GTX 480为例,它内部的SIMD处理单元宽度为16,整个核心拥有16个这样的处理单元,一个时钟周期就能完成256FLOPS的运算量,这个并行计算的宽度是惊人的,但是强大的并行能力背后需要寄存器或者存储器系统的支持。如果每个操作数都由内存提供,那么每进行一次单精度计算都需要内存系统提供2KB数据,显然这是难以达到的,如此大的存储访问压力需要使用其他技术进行分摊。
因此,GPGPU通常具备数量庞大的通用寄存器组,尽量使数据保存在本地,同时每个SIMD处理单元都具备多线程的硬件支持,例如Fermi的每个SIMD内部都设立了一个包含48线程的调度表,能够在当前线程发生存储访问停顿时迅速切换至其他线程,通过多个线程的重叠执行来隐藏存储访问延迟。除此之外,GPGPU的内存控制器也支持Gather-Scatter(分散-集中)操作,能够应对部分不规则的内存位置访问,对接的内存系统也是经过特别设计以实现更高带宽。从图中可以看出,GPGPU核心内部的缓存所占面积也很小,绝大部分芯片面积都用来构建逻辑运算单元和数据通路。相较之下,CPU则依靠更大的缓存来弥补内存缺憾,缓存所占芯片面积达到50%是常事,历史上甚至出现过HPPA-8700这样的奇葩架构,缓存所占面积几乎达到四分之三。
经过这样设计的GPGPU架构,其控制逻辑相对简单,一条指令便可以控制极宽的数据通路,而数据通路可以按照需求进行关闭,缩减功耗,因此这种架构能够带来更好的能耗表现,这便是架构设计师们对功耗墙问题所做的应对措施之一。再来看访存墙,GPGPU的线程并行度很高,来回切换线程的做法能更好地容忍内存延迟,但是对内存带宽需求更高,而且要求存储访问更加局部化。当计算任务的相关性很弱,几乎不存在分支,且内存访问模式比较规律的时候,GPGPU能够以更快的速度执行完毕,这部分优势是GPGPU截然不同的设计思路所带来的,也是传统CPU难以企及的,在GPGPU上实现较高计算加速比的通常就是这类程序。但是GPGPU也并非万能,它处理分支的能力很弱,目前大多依靠谓词执行,如果让GPGPU来处理分支较多的程序,SIMD单元就会在各种指令路径间来回切换,效率大打折扣。例如,给定一个宽度为16的向量,根据每个数字的奇偶性来决定执行路径,偶数则执行乘法,奇数则执行除法,那么就无法再依靠单一指令指挥整个向量数据流了,而这种工作在CPU上却得心应手。此外,若计算任务出现较强的相关性,程序员就很难编写并行化的代码,这部分任务也很难在GPGPU上执行。因此,可以预见CPU+GPGPU需要在以后的时间里分工合作,让CPU凭借强大的分支预测和快速串行执行能力来处理串行化代码,而GPGPU处理并行化的代码,以这种异构计算的方式实现更高的效率,NVIDIA的CUDA编程语言和GPGPU执行架构便是基于这样的思路设计,那么作为业界巨头的英特尔,又是如何做的呢?