写在前面的话:因为英语不好,所以看得慢,所以还不如索性按自己的理解简单粗糙翻译一遍,就当是自己的读书笔记了。不对之处甚多,以后理解深刻了,英语好了再回来修改。相信花在本书上的时间和精力是值得的。
————————————————————————————————
“计算机大部分时候就是你所看到的”--黄仁勋
图形加速一开始应用于扫描三角形时像素的插值计算,把像素值绘制在屏幕上,纹理访问图像数据并应用到表面上。后来又加了对深度值(z-depths)的插值和深度测试。由于使用的频繁,这些处理都被委托给专门的硬件来支持以提高性能。渲染管线的很多功能都是逐步迭代起来的。专门的图形加速硬件对于CPU唯一的优势就是处理速度,然而处理速度相当重要。
在过去的二十年,图形硬件经历了飞速发展。第一款有顶点处理功能的商用图形芯片(NVIDIA’s GeForce256)发布于1999年。英伟达为了区分GeForce256和以前的光栅化芯片,以GPU(graphics processing unit)来命名它,自此以后都开始叫GPU了。接下来几年里,GPU由复杂的固定渲染管线发展到了由开发者可编程实现。各种各样的可编程着色器是控制GPU的主要途径。为了效率,部分管线仍保持着可配,不可编程,但是都是朝着可编程和灵活性发展。
由于GPU是高度并行化处理任务,所以GPU有着很快的速度。它有专门组成部分来处理实现z-buffer,可快速访问纹理图像和其他的缓冲,可快速找到对应像素。23章将会详细讲解这些,这里主要讲的是为什么GPU拥有高度并行性。
3.3节解释了着色器的工作原理。目前,我们只需要知道着色器核心是一个小处理器,可用来处理相对独立的任务,例如将顶点由模型坐标转换到世界坐标,计算像素的颜色等等。由于每帧需要给屏幕提供成千上万个三角形,每秒有着几亿次的着色器调用(shader invocations)。
延迟是所有处理器都需要面对的问题。访问数据需要花费一定时间。如果数据信息到处理器的时间越长延迟就越大。章节23.3有详细介绍这个。访问存储器里的数据比访问寄存器里的数据需要更多的时间。章节18.4.1详细讲到了内存访问。等待检索数据会降低性能。
3.1 数据并行结构
为了提高速度,每种处理器都有对应的策略。CPU通常被用来处理各种各样的数据结构和大型代码块。CPU的多处理器,除了有限的单指令多数据数据结构(SIMD)向量处理外,他们都以串行的方式运行代码。为了减少延迟,大部分CPU芯片都有快速的局部缓存,存储着接下来有可能需要的数据。CPU同样有一些技巧减少延迟,例如分支预测,指令重排, 寄存器重命名和缓存预取。
GPU采用不一样的方法。大部分GPU芯片都是由几千个着色器组成。GPU是一个流处理器,相似的数据依次通过处理。正是由于相似性,GPU可以用大规模并行的方式处理这些数据。另外这些调用都是尽可能独立的,并不需要彼此之间的信息,没有共享内存。但是有时候这个规则会被打破,为了一些新的有用的功能,以牺牲一些性能为代价,不得不等待另外一个处理器完成工作。
GPU用吞吐量(throughput)来描述最大的数据处理速度。然而,这个快速处理有代价,由于很少芯片区域是可以缓存内存或控制逻辑,每个着色器的延迟通常要比CPU处理器的延迟高。
假设一个Mesh被光栅化之后有两千个片元需要被处理,像素着色程序需要被调用两千次。想象一下,如果只有一个着色器,性能肯定很糟糕。着色器每处理一个片元都需要在寄存器中完成一些数学操作。因为寄存器是局部的,快速的,所以访问很快。假如着色器需要访问一张纹理来知道mesh上像素的颜色。纹理是一个完全独立的资源,并不在像素着色程序的局部内存中,访问纹理是需要一定操作的。访问内存是需要成千上万个时钟周期的,而在这段时间内GPU是没有事情可做的。这时候着色器就在阻塞,等待着纹理颜色数据传过来。
为了让糟糕的GPU变得更好,可以给每个片元的局部寄存器一点存储空间。这样,与其在等待纹理数据,可以切换着色器去执行另外一个片元了。除了需要指出在第一个片元中执行的指令外,这个切换对两个片元的执行都没有影响,速度是很快的。和第一个片元一样,第二个片元同样有数学操作,然后获得纹理数据。第二个片元处理完,会紧接着处理第三个片元,直到两千个片元都以这种方式处理完。此时,着色器会回到第一个片元。到这时,所有的纹理数据都获取到了,等待着使用,这样着色程序可以继续执行。处理器会以这种方式处理下去,直到遇到下一个会阻塞,或者程序完成。单个片元的处理时间可能会增加,但是整体上片元处理时间大大减少。
在这个结构中,GPU由一开始的等待阻塞变成了去处理接下来的片元。GPU进一步设计将逻辑指令从数据中分离开,称为单指令多数据结构(SIMD),这种结构会在固定数量的着色器中以lock-setp的形式运行相同的指令。和运用单独的逻辑或调度单元器运行每个程序相比,SIMD的优势是可以用更少的硬件去处理数据和交互数据。将两千个片元处理例子用在现在GPU上,每个像素着色处理一个片元的过程叫一个线程(thread)。这个线程不同CPU中的线程,在着色器处理过程中的任何寄存器都需要一点内存来存储输入值。运行相同着色程序的线程被捆绑成一组,在NVIDIA中称为warps,在AMD中称为wavefronts。一个warps或wavefronts,是同时用8到64个GPU着色器运行SIMD处理过程。每一个线程都被映射到一个SIMD lane中。
如果我们有2000个片元需要操作,NVIDIA中有32个线程,那么每个线程就有2000/32 = 62.5个warps,这意味着需要63个warps,其中最后一个有一半是空的。warp的处理过程和单个GPU处理过程类似,32个处理单元在同一个lock-step上运行,一旦有一个执行拿取内存,其他的处理器都会同时执行相同操作,因为所有的处理器都是执行一样的指令。如果一个warp中拿取内存遇到了阻塞,后续的操作都需要等待它的数据,为了应对这种情况,我们可以将后续工作切换给另外一个warp。这个切换和我们单线程处理一样快,Wrap之间切换并无额外的开销,每个线程都有着自己的寄存器,每个warp都可以跟踪正在执行的指令。切换到一个新的warp只需要将一组核心指向另外一组核心,只有极小的开销。warp执行或切换直至全部工作完成。见图3.1。
图3.1 简化了的着色器处理例子。 一个三角形的全部片元,或者称为一个线程(threads),分成组warps。每个warp展示有4个线程,实际上有32个线程。这个着色程序有5个指令。GPU着色器执行这些指令从第一个warp开始,直到发现“txr”指令遇到了阻塞,需要时间去获取数据。第二个warp切换进来,着色器程序的前三个指令提交给的第二个warp,直到再遇到阻塞。紧接着第三个warp会切进来。在第三个warp遇到阻塞后,会切换到第一个warp,继续执行。如果这时候他的“txr”指令数据仍未拿到,执行真正的阻塞直到拿到所有的数据。每个warp依次完成。
在上面简单的例子中,warp的切换实际上是有一点开销的,尽管开销很小。尽管还有其他的技术来做优化执行效率,但是warp切换(warp-swapping)仍然是GPU降低延迟最主要的方法。还有几个因素影响处理过程的效率,例如有多少线程,多少warp可以被创建。
着色程序的结构同样是影响效率的重要因素。一个主要的因素是每个线程用到的寄存器的数量。在上面的例子里,我们假设GPU一次性需要处理两千个片元,每个线程需要的寄存器越多,GPU常驻线程就越少,warp也就越少。一旦warp少了,意味着遇到阻塞可切换的机会变少。warp都处在活跃中,活动的warp数量和最大数量的比值occupancy高(GPU占有率)。occupancy高意味着,更多地warp可用,所以空闲的处理器就很少。低occupancy则会经常导致低性能。获取内存的频率同样影响延迟。
另外一个因素影响整体效率的是动态分支(dynamic branching),由“if”语句和循环导致。假设在着色程序中碰到“if”语句。如果所有的线程都是一个分支里,warp不需要考虑到其他的分支执行,然而,在一些线程中,甚至只有一个线程,一旦有分支,warp必须两个分支都执行,然后通过特定的线程丢弃不需要的结果。这个问题称为,线程散度(thread divergence),warp中少量的线程需要执行循环迭代或者if分支,而warp中的其他线程不需要执行,则会导致这部分不需要执行的线程处于闲置阶段。
在接下来的章节中,我们将讨论GPU如何实现渲染管线,可编程着色器如何操作,每个GPU阶段的功能和延伸。
3.2 GPU渲染管线概述
GPU由几何处理阶段、光栅化阶段和像素处理阶段组成。而这些又被分成不同程度可配置或可编程的子阶段。图3.2展示了各种阶段,用颜色区分了是否可配可编程。注意这些物理阶段的划分可能跟第二章中的划分不同。
图3.2 GPU的渲染管线组成。 这些阶段按照颜色划分是否可编程可配置。绿色阶段是完全可编程的,虚线是可选阶段,黄色阶段是可配置的但不能编程的,例如在合并阶段的各种混合模式。蓝色阶段是固定功能。
GPU的逻辑模型,通过API暴露给开发者。正如18章和23章讨论的,逻辑管线(物理模型)的实现有硬件厂商提供。逻辑模型中的固定功能可以通过在相邻的可编程阶段添加指令执行。在渲染管线中一段程序可以被好几个子单元执行不同的代码段,也可以被一段特定的pass完整执行。逻辑模型会帮助你理解什么会影响性能,但是不应该被认为是GPU实现渲染管线的方式。
顶点着色,组成几何处理阶段的一部分,是一个完全可编程阶段。几何着色阶段同样是完全可编程阶段,用来处理图元的顶点,可以操作每个图元的着色,可销毁图元,可以创建新的图元。曲面细分和几何着色都是可选阶段,并不是所有的GPU都支持,特别是在移动设备中。
裁剪,三角形设置和三角形遍历都是硬件的固定功能。窗口和视口的设置会影响到屏幕映射。像素处理阶段是完全可编程的。尽管合并阶段不是可编程的,但是它高度可配,通过一系列参数设置。它的功能有改变颜色值,深度缓冲,混合,模板测试和其他缓冲等等。像素着色和合并一起组成了像素处理阶段。
随着时间推移,GPU管线从硬编码操作朝着越来越灵活越来越可控发展。其中可编程阶段是重要的一环。下一节,将会介绍各种可编程阶段的特征。
3.3 可编程着色阶段
现代着色程序采用统一的着色设计。这意味着顶点,像素,几何和曲面细分等相关着色处理器都采用了一个通用的编程模型。他们有着相同的指令系统体系结构(ISA, instruction set architecture)。由这种模型构成的处理器称为通用着色器核心,而有这样核心的GPU,就有着统一的着色结构。在这种结构后面的思想是,着色器处理是可以被各种角色使用的,并且GPU可以根据需求来对应分配。举个例子,一个拥有细小三角形构成的mesh要比两个三角形构成的大四边形需要更多顶点着色。一个分别拥有顶点着色核心池和像素着色核心池的GPU,理想的工作分配是保持让这些着色器核心有预测的处于忙碌中。GPU拥有统一着色器核心,就可以决定如何平衡这条路。