|
从历史上看,图形加速始于在与三角形重叠的每条像素扫描线上插入颜色,然后显示这些值。包括访问图像数据的功能使得纹理可以应用于表面。添加用于内插(interpolating)和测试z深度的硬件,提供了内置的可见性检查。由于它们的频繁使用,此类过程便使用专用硬件以提高性能。渲染管线的更多部分以及更多功能随着硬件代数提升被加入进来。专用图形硬件相对于CPU的唯一计算优势是速度,但速度至关重要。
在过去的二十年中,图形硬件经历了不可思议的变革。 1999年,第一款具有硬件顶点处理功能的消费类图形芯片(NVIDIA的GeForce256)问世。NVIDIA创造了术语图形处理单元(GPU),以将GeForce 256与以前可用的仅光栅化的芯片区分开来。在接下来的几年中,GPU从可配置实现的复杂的固定功能管线发展为可以由开发人员在其中实现自己的算法的高度可编程空白板(blank)。各种可编程着色器是控制GPU的主要方法。为了提高效率,管线的某些部分仍然是可配置而不可编程的,但是趋势是朝着可编程性和灵活性的方向发展[175]。
GPU通过专注于一组高度可并行化的任务而获得了卓越的速度。他们拥有专用的定制芯片,可以实现z缓冲区,可以快速访问纹理图像和其他缓冲区以及查找哪些像素被三角形覆盖。这些部件如何执行其功能将在第23章中介绍。更重要的是我们需要尽早知道的是GPU如何为其可编程着色器实现并行性。
第3.3节介绍了着色器的功能。现在,您需要知道的是着色器核心是一个小型处理器,可以执行一些相对独立的任务,例如将顶点从其在世界中的位置转换为屏幕坐标,或计算被三角形覆盖的像素的颜色。当每帧有成千上万的三角形发送到屏幕,每秒可以进行数十亿次着色器调用(shader invocations),这就是运行着色器程序的单独实例。
首先,延迟(latency)是所有处理器都面临的问题。访问数据需要花费一些时间。考虑延迟的一种基本方法是,信息与处理器之间的距离越远,等待的时间就越长。第23.3节详细介绍了延迟。访问存储在内存芯片中的信息将比访问存储在本地寄存器中的信息花费更多的时间。第18.4.1节将更深入地讨论内存访问。关键是等待数据被检索意味着处理器停滞了,这降低了性能。
3.1 数据并行架构
不同的处理器体系结构使用各种策略来避免停顿。 CPU经过优化,可处理各种数据结构和大型代码库。 CPU可以具有多个处理器,但是每个处理器都以串行方式运行代码,有限的SIMD矢量处理是一个小例外。为了最大程度地减少延迟的影响,CPU的大部分芯片都由快速的本地缓存组成,内存中将填充接下来可能需要的数据。 CPU还通过使用诸如分支预测,指令重新排序,寄存器重命名和缓存预取之类的巧妙技术来避免停顿。[715]
GPU采用不同的方法。 GPU的大部分芯片区域被设计填充了大量处理器,它们被称为着色器核心(shader cores),通常有数千个。 GPU是流处理器,它会依次处理相似数据的有序集合。由于这种相似性(例如,一组顶点或像素),GPU可以大规模并行地处理这些数据。另一个要素是这些调用尽可能地独立,这样它们就不需要来自相邻调用的信息,并且不共享可写的内存位置。该规则有时会被破坏以允许新的有用功能,但是由于一个处理器可能等待另一个处理器完成其工作,因此这种例外会带来潜在的延迟。
GPU针对吞吐量(throughput)进行了优化,吞吐量被定义为可以处理数据的最大速率。但是,这种快速处理具有成本。由于被设计用于缓存内存和控制逻辑的芯片区域较小,因此每个着色器核心的等待时间通常比CPU处理器遇到的等待时间久得多[462]。
假设网格已光栅化(rasterized),并且有两千个像素具有要处理的片元;一个像素着色器程序将被调用两千次。想象只有一个着色器处理器,世界上最弱的GPU。它开始为两千个片段中的第一个片段执行着色器程序。着色器处理器对寄存器中的值执行一些算术运算。寄存器是本地的且可以快速访问,因此不会发生停顿。然后,着色器处理器会收到一条指令,例如纹理访问;例如,对于给定的表面位置,程序需要知道应用于网格的图像的像素颜色。一张纹理是一个完全独立的资源,而不是像素程序本地内存的一部分,并且可能会涉及到一定纹理访问。内存提取可能需要数百到数千个时钟周期,在此期间GPU处理器不执行任何操作。此时,着色器处理器将停顿,等待返回纹理的颜色值。
为了使这个糟糕的GPU变得更好,因此为每个片元提供一些用于其本地寄存器的存储空间。现在,允许着色器处理器切换并执行另一个片元,即执行两千个片元中的第二个片元,而不是停止纹理获取。这个切换速度非常快,除了注意第一个指令正在执行哪个指令外,第一个或第二个片段中的任何内容均不受影响。现在执行第二个片元。与第一个相同,执行一些算术函数,然后再次遇到纹理获取。着色器核心现在再切换到第三个片段。最终,所有两千个片段都以这种方式处理。此时,着色器处理器将返回片元编号1。此时,纹理颜色已被获取并可以使用,因此着色器程序可以继续执行。处理器以相同的方式进行处理,直到遇到另一个已知会暂停执行的指令或直到程序完成为止。单个片元的执行时间要比着色器处理器始终专注于该片元的执行时间长,但所有片元的总体执行时间大大减少。
在这种架构中,GPU通过切换到另一个片元的方式保持忙碌来隐藏延迟。 GPU通过将指令执行逻辑与数据分离开来,使该设计更进一步。称为单指令,多数据(single instruction, multiple data, SIMD, 以下简称SIMD),这种安排在固定数量的着色器程序上以锁定步骤(lock-step)的方式的执行同一命令。 SIMD的优势在于,与使用单个逻辑和调度单元运行每个程序相比,用于处理数据和交换的芯片(和功率)要少得多。将我们的两千个片元示例转换为现代GPU术语,每个像素着色器对一个片元的调用称为一个线程。这种类型的线程与CPU线程不同。它由用于着色器输入值的一点内存与着色器执行所需的所有寄存器空间组成。使用相同着色器程序的线程被打包成组,NVIDIA称之为warps,AMD称为wavefronts。一个warps/wavefronts被预定使用一些GPU着色器核心进行SIMD处理,核心数量从8个到64个不等。每个线程都映射到一个SIMD通道(SIMD Lane)。
假设我们有两千个线程要执行。 NVIDIA GPU的一个warp包含32个线程,这将产生2000/32 = 62.5个warps,这意味着分配了63个warps,其中一个warps内有一半为空。单个warp的执行类似于我们的单个GPU处理器示例。着色器程序在所有32个处理器上均以锁定步骤执行。遇到内存提取时,因为所有线程执行相同的指令,因此所有线程都同时遇到它。提取信号表明这个warp中的所有线程将停止,所有线程都在等待它们的(不同)结果。此时会将warp换成另一个32线程的warp,然后由32个核心执行而不是停滞。这种交换与我们的单处理器系统一样快,因为在将warp换入或换出时,每个线程内的数据都不会被触及。每个线程都有自己的寄存器,每个warp都跟踪其正在执行的指令。换入新warp只是将一组核心指向要执行的另一组线程。没有其他开销。warp会执行或换出直到全部完成。参见图3.1
在我们的简单示例中,纹理获取内存的延迟可能导致warp换出。实际上,因为交换的成本非常低,所以warp换出可以有更短的延迟。还有其他几种用于优化执行的技术[945],但warp交换(warp-swapping)是所有GPU使用的主要的隐藏延迟的机制。此过程的效率涉及几个因素。例如,如果线程很少,那么几乎不会创建任何warp,从而使隐藏延迟成为问题。
着色器程序的结构是影响效率的重要特征。一个主要因素是每个线程使用的寄存器数量。在我们的示例中,我们假设一次可以将2000个线程全部驻留在GPU上。与每个线程相关联的着色器程序所需的寄存器越多,则可以在GPU中驻留更少的线程,从而减少warp。 warps的不足意味着无法通过交换来减轻失速。驻留的warps被称为“飞行中”,这个驻留数量称为占用率。高占用率意味着有许多可用于处理的线程束,因此不太会有空闲处理器。占用率低通常会导致性能不佳。内存获取的频率也会影响需要隐藏的延迟时间。 Lauritzen [993]概述了占用率受寄存器数量和着色器共享内存的影响。 Wronski [1911,1914]讨论了理想的占用率如何根据着色器执行的操作类型而变化。
影响整体效率的另一个因素是由“ if”语句和循环引起的动态分支。假设在着色器程序中遇到“ if”语句。如果所有线程都求值并采用同一分支,则warp可以继续进行而不必担心其他分支。但是,一旦某些线程甚至只有一个线程采用了替代路径,那么warp必须执行两个分支,然后丢弃每个特定线程不需要的结果[530,945]。这个问题称为线程分歧/线程发散(thread divergence),其中一些线程可能需要执行循环迭代或执行warp中其他线程不需要的“ if”路径,从而使它们在此期间处于空闲状态。
所有GPU都实现了这些架构思想,从而导致系统受到严格限制,但是每瓦特有大量的计算能力。了解此系统的运行方式将帮助您作为程序员更有效地利用其提供的功能。在接下来的部分中,我们将讨论GPU如何实现渲染管线、可编程着色器如何操作以及每个GPU阶段的演变和功能。
图3.1 简化的着色器执行示例。一个三角形的所有片元,调用线程,集成到warps。每个warp显示为4个线程,但实际上有32个线程。要执行的着色器程序有五条指令长。由4个GPU着色器处理器组成的集合执行第一次warp的指令,直到在“txr”命令上检测到暂停条件,这需要时间来获取其数据。换入第二个warp,应用着一个warp继续执行。如果它的“txr”命令的数据此时还没有返回,那么执行将真正的暂停,直到这些数据可用为止。每一个warp依次结束。
[175] Blythe, David, “The Direct3D 10 System,”ACM Transactions on Graphics, vol. 25, no. 3,pp. 724–734, July 2006.Cited on p. 29, 39, 42, 47, 48, 50, 249
[715] Hennessy, John L., and David A. Patterson, Computer Architecture: A Quantitative Approach, Fifth Edition, Morgan Kaufmann, 2011. Cited on p. 12, 30, 783, 789, 867, 1007, 1040
[462] Fatahalian, Kayvon, and Randy Bryant, Parallel Computer Architecture and Programming course, Carnegie Mellon University, Spring 2017. Cited on p. 30, 55
[945] Kubisch, Christoph, “Life of a Triangle—NVIDIA’s Logical Pipeline,” NVIDIA GameWorks blog, Mar. 16, 2015. Cited on p. 32
[993] Lauritzen, Andrew, “Future Directions for Compute-for-Graphics,” SIGGRAPH Open Problems in Real-Time Rendering course, Aug. 2017. Cited on p. 32, 812, 908
[1911] Wronski, Bartlomiej, “Assassin’s Creed: Black Flag—Road to Next-Gen Graphics,” Game Developers Conference, Mar. 2014. Cited on p. 32, 218, 478, 571, 572, 801
[1914] Wronski, Bartlomiej, “GCN—Two Ways of Latency Hiding and Wave Occupancy,” Bart Wronski blog, Mar. 27, 2014. Cited on p. 32, 801, 1005
[530] Giesen, Fabian, “A Trip through the Graphics Pipeline 2011,” The ryg blog, July 9, 2011. Cited on p. 32, 42, 46, 47, 48, 49, 52, 53, 54, 55, 141, 247, 684, 701, 784, 1040
[945] Kubisch, Christoph, “Life of a Triangle—NVIDIA’s Logical Pipeline,” NVIDIA GameWorks blog, Mar. 16, 2015. Cited on p. 32
3.2 GPU管线概览
GPU实现了概念如几何处理、光栅化和像素处理管线阶段,如第2章所述。它们分为几个硬件阶段,每个阶段具有不同程度的可配置性或可编程性。图3.2显示了根据可编程或可配置程度对不同阶段进行颜色编码。请注意,这些物理阶段的划分与第2章中介绍的功能阶段略有不同。
图3.2 渲染管线的GPU实现。这些阶段根据用户对其操作的控制程度进行颜色编码。绿色的阶段完全可编程。虚线表示可选阶段。黄色阶段可配置,但不可编程,例如,可为Merger阶段设置各种混合模式。蓝色阶段的功能是完全固定的。
我们在这里描述GPU的逻辑模型,它是以API的方式公开给程序员的。正如第18章和第23章所讨论的,这个逻辑管线(物理模型)的实现取决于硬件供应商。逻辑模型中功能固定的阶段可以通过向相邻的可编程阶段添加命令来在GPU上执行。管线中的单个程序可以拆分为由单独的子单元执行的元素,也可以完全由单独的pass执行。逻辑模型可以帮助您分析影响性能的因素,但不应将其误认为GPU实际实现管线的方式。
顶点着色器是一个完全可编程的阶段,用于实现几何处理阶段。几何着色器是一个完全可编程的阶段,可在图元的顶点(点,线或三角形)上运行。它可以用于执行以图元单位的着色操作,可以销毁图元或创建新图元。细分阶段和几何着色器都是可选的,并非所有GPU都支持它们,尤其是在移动设备上(可能不支持)。
裁剪、三角形设置和三角形遍历阶段由固定功能硬件实现。屏幕映射受窗口和视口设置的影响,在内部形成简单的比例并重新定位。像素着色器阶段是完全可编程的。尽管合并阶段不是可编程的,但它的高可配置性可以设置执行多种操作。它实现了“合并”功能阶段,负责修改颜色、z缓冲区、混合、模板以及任何其他与输出相关的缓冲区。像素着色器的执行与合并阶段一起构成了第2章介绍的概念性像素处理阶段。
随着时间的推移,GPU管线已经从硬编码操作发展到逐步增加的灵活性和可控性。可编程着色器的引入是这一发展过程中最重要的一步。下一节描述了各个可编程阶段共有的功能。
3.3 可编程着色器阶段
现代着色器程序使用统一的着色器设计。这意味着与顶点,像素,几何和曲面细分相关的着色器共享一个通用的编程模型。在内部,它们具有相同的指令集体系结构(ISA)。在DirectX中,实现此模型的处理器称为通用着色器核心,具有这种核心的GPU被称为具有统一的着色器体系架构。这种类型的体系架构背后的想法是,着色器处理器可以在各种任务中使用,GPU可以根据需要分配它们。例如,与每个由两个三角形组成的大正方形相比,一组带有小三角形的网格将需要更多的顶点着色器处理。一个具有由顶点着色器和像素着色器核心组成的一组池的GPU意味着它严格确定了理想的工作分配使所有核心保持繁忙。使用统一的着色核心,GPU可以决定如何平衡此负载。
描述整个着色器编程模型远远超出了本书的范围,并且已经有许多文档,书籍和网站。着色器使用类似于C语言的着色语言进行编程,例如DirectX的高级着色语言(HLSL)和OpenGL着色语言(GLSL)。 DirectX的HLSL可以编译为虚拟机字节码,也称为中间语言(IL或DXIL),以提供硬件独立性。中间表示也可以允许着色器程序被离线编译和存储。驱动程序将此中间语言转换为特定GPU的ISA。控制台编程通常避免中间语言步骤,因为那时系统只有一个ISA。
基本数据类型是32位单精度浮点标量和向量,尽管向量只是着色器代码的一部分,并且在上面概述的硬件中不受支持。在现代GPU上,本地还支持32位整数和64位浮点数。浮点向量通常包含诸如位置(xyzw),法线,矩阵行,颜色(rgba)或纹理坐标(uvwq)之类的数据。整数通常用于表示计数器,索引或位掩码。还支持聚合数据类型,例如结构,数组和矩阵。
一次绘制调用(draw call)调用图形API来绘制一组图元,从而导致图形管线执行并运行其着色器。每个可编程着色器阶段都有两种类型的输入:统一输入,其值在整个绘制调用期间保持恒定(但可以在绘制调用之间更改),以及可变输入,是来自三角形的顶点或光栅化的数据。例如,像素着色器会以统一的值提供光源的颜色,而三角形表面的位置会随着像素的变化而变化。纹理是一种特殊的统一输入,它曾经是单纯的应用于表面的彩色图像,但现在可以把它视作是任何大型数据数组。
底层虚拟机为不同类型的输入和输出提供特殊的寄存器。用于统一的可用常数寄存器的数量比用于变化输入或输出的可用寄存器的数量大得多。这是因为需要为每个顶点或像素分别存储变化的输入和输出,因此对于它们的需求存在自然的限制。统一输入存储一次之后会在绘制调用中的所有顶点或像素之间重复使用。虚拟机还具有用于暂存空间的通用临时寄存器,可以使用临时寄存器中的整数值对所有类型的寄存器进行数组索引,着色器虚拟机的输入和输出如图3.3所示。
图3.3 在Shader Model 4.0下的统一虚拟机架构和寄存器布局。每个资源旁边会显示最大可用数量。用斜杠分隔的三个数字是指顶点,几何和像素着色器的限制(从左到右)
图形计算中常见的操作可在现代GPU上高效执行。着色语言通过*和+等运算符将最常见的操作暴露出来(对应加法和乘法)。其余的则通过内联函数(intrinsic function)公开,例如atan(),sqrt(),log()以及许多其他针对GPU优化的函数。GPU还存在用于更复杂的操作的功能,例如向量归一化和反射,叉积,矩阵转置和行列式计算。
术语流控制(flow control)是指使用分支指令来更改代码执行流。与流控制有关的指令用于实现高级语言结构,例如"if"和"case"语句,以及各种类型的循环。着色器支持两种类型的流控制。静态流控制分支基于统一输入的值。这意味着代码流在绘图调用中是恒定的。静态流控制的主要好处是可以将相同的着色器用于各种不同的情况(例如,数量不等的灯光)。因为所有调用都采用相同的代码路径所以没有线程分歧。动态流控制基于可变输入的值,这意味着每一个片元可以不同的执行代码。这比静态流控制功能强大得多,但会降低性能,尤其是在着色器调用之间代码流不规律地更改的情况下。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|