GPU体系架构(一):数据的并行处理

最近在了解GPU架构这方面的内容,因为资料零零散散,因此准备写两篇博客整理一下。GPU的架构复杂无比,这两篇文章也是从宏观的层面去一窥GPU的工做原理罢了缓存

 

GPU根据厂商的不一样,显卡型号的不一样,GPU的架构也有差异,可是大致的设计基本相同,原理的部分也是相通的。下面咱们就以NVIDIA的Fermi架构为蓝本,从下降延迟的角度,来说解一下GPU究竟是如何利用数据的并行处理来提高性能的。有关GPU的架构细节和逻辑管线的实现细节,咱们将在下一篇里再讲。架构

 

不管是CPU仍是GPU,都在使用各类各样的策略来避免停滞(stall)。app

 

CPU的优化路线有不少,包括使用pipeline,提升主频,在芯片上集成访问速度更快的缓存,减小内存访问的延迟等等。在减小stalls的路上,CPU还采用了不少聪明的技术,好比分支预测,指令重排,寄存器重命名等等。性能

 

GPU则采用了另外一种不一样的策略:throughput。它提供了大量的专用处理器,因为GPU端数据的自然并行性,因此经过数据的大规模并行化处理,来下降延迟。这种设计优势是经过提升吞吐量,数据的总体处理时间减小,隐藏了处理的延迟。可是因为芯片上集成的核越多,留给其余设备的空间就越小,因此像memory cache和logical control这样的设备就会变少,致使每一路shader program的执行变得延迟很高。了解这个特性,咱们来看一个例子,以此来讲明如何利用GPU的架构,写出更高效的代码。优化

 

假如咱们有一个mesh要被渲染,光栅化后生成了2000个fragment,那么咱们须要调用一个pixel shader program 2000次,假如咱们的GPU只有一个shader core(世界最弱鸡GPU),它开始执行第一个像素的shader program,执行一些算数指令,操做一下寄存器上的值,因为寄存器是本地的,因此此时并不会发生阻塞,可是当程序执行到某个纹理采样的操做时,因为纹理数据并不在程序的本地寄存器中,因此须要进行内存的读取操做,该操做可能要耗费几百甚至几千个时钟周期,因此会阻塞住当前处理器,等待读取的结果。若是真的只是这样设计这个GPU,那它真的就是太弱鸡了,因此为了让它稍微好点,咱们须要提高它的性能,那如何下降它的延迟呢?咱们给每一个fragment提供一些本地存储和寄存器,用来保存该fragment的一些执行状态,这样咱们就能够在当前fragment等待纹理数据时,切换到另外一个fragment,开始执行它的shader program,当它遇到内存读取操做阻塞时,会再次切换,以此类推,直到2000个shader program都执行到这里。这时第一个fragment的颜色已经返回,能够继续往下执行了。使用这种方式,能够最大化的提升GPU的效率,虽然在单个像素来看,执行的延迟变高了,可是从2000个像素总体来讲,执行的延迟减小了。编码

 

现代GPU固然不会弱鸡到只有一个shader core,可是它们也一样采用了这种方式来减低延迟。现代GPU为了提升数据的并行化,使用了SIMT(Single Instruction Multi Thread,SIMD的更高级版本),执行shader program的最小单位是thread,执行相同program的threads打包成组,NVIDIA称之为warp,AMD称之为wavefront。一个warp/wavefront在特定数量的GPU shader core上调度执行,warps调度器调度的基本单元就是warp/wavefront。spa

 

假如咱们有2000个fragment须要执行shader program,以NVIDIA为例,它的GPU包含32个thread,因此要执行这些任务须要2000/32 = 62.5个warps,也就是说要分配63个warps,有一个只使用一半。线程

一个warp的执行过程跟单个GPU shader core的执行过程是相似的,32个像素的shader program对应的thread,会在32个GPU shader core上同时以lock-step的方式执行,当着色器程序遇到内存读取操做时,好比访问纹理(很是耗时),由于32个threads执行的是相同的程序,因此它们会同时遇到该操做,内存读取意味着该线程组将会阻塞(stall),所有等待内存读取的结果。为了下降延迟,GPU的warp调度器会将当前阻塞的warp换出,用另外一组包含32个线程的warp来代替执行。换出操做跟单核的任务调度同样的快,由于在换入换出时,每一个线程的数据都没有被触碰,每一个线程都有它本身的寄存器,每一个warp都负责记录它执行到了哪条指令。换入一个新的warp,不过是将GPU 的shader cores切换到另外一组线程上继续执行,除此以外没有其余额外的开销。该过程以下图所示:设计

 

在咱们这个简单的例子中,内存读取的延迟(latency)会致使warp被换出,在实际的应用中,可能更小的延迟就会致使warp的换出操做,由于换入换出的操做开销很是低。warp-swapping的策略是GPU隐藏延迟(latency)的主要方式。可是有几个关键因素,会影响到该策略的性能,好比说,若是咱们只有不多的threads,也就是只能建立不多的warp,会使隐藏延迟出现问题。3d

 

shader program的结构是影响性能的主要角色,其中最大的一个影响因素就是每一个thread须要的寄存器的数量。在上面例子的讲解过程当中,咱们一直假设例子中的2000个thread都是同时驻留在GPU中的。可是实际上,每一个thread绑定的shader program中须要的寄存器越多,产生的threads就越少(由于寄存器的数量是固定的),可以驻留在GPU中的warp就越少。warps的短缺也就意味着没法使用warp-swapping的策略减缓延迟。warps在GPU中存在的数量称之为占用率,高占用率意味着有更多的warps能够用来执行,低占用率则会严重影响GPU的并行效率。

 

另外一个影响GPU性能的因素就是动态分支(dynamic branching),主要是由if和循环引进的。由于一个warp中的全部线程在执行到if语句时,就会出现分裂,若是你们都是执行的相同的分支,那也没什么,但但凡是有一个线程执行另外一个分支,那么整个warp会把两个分支都执行一遍,而后每一个线程扔掉它们各自不须要的结果,这种现象称为thread divergence

 

了解了上面的基本原理,咱们能够看出,整个GPU的设计其实也是一种trade-off,用单路数据的高延迟,来换总体数据的吞吐量,以此来最大化GPU的性能,下降stall。在实际的编码过程当中,尤为是shader的编写过程当中,也要严肃影响GPU优化策略的几个因素,只有这样,才能写出更加高效的代码,真正发挥出GPU的潜力。

 

下一篇会更加详细的介绍GPU的结构和逻辑管线,若是错误,欢迎指正。

相关文章
相关标签/搜索