找回密码
 立即注册
查看: 375|回复: 0

第三章 图形处理器(上)

[复制链接]
发表于 2021-7-29 13:59 | 显示全部楼层 |阅读模式
"The display is the computer."
--Jen-Hsun Huang
历史上,图形加速始于在与三角形重叠的每个像素扫描线上对颜色插值,然后显示这些值。包括访问图像数据将纹理应用于表面的能力。为插值和z深度测试添加硬件提供内置的可见性检测。因为它们经常使用,这些处理被提交到专门的硬件用以提升性能。渲染管道的更多部分,每部分的更多功能,被加到后续的硬件中。专用图像硬件相对CPU的唯一计算优势是速度,但是速度很关键。
在过去的二十年中,图形硬件经历了不可思议的转变。第一款包含硬件顶点处理的消费类图形芯片(NVIDIA的GeForce256)于1999年发售。NVIDIA创造了术语图形处理单元(GPU),以区分GeForce 256和以前可用的光栅化芯片,并且沿用下来。在接下来的几年中,GPU从可配置复杂的固定功能流水线到高度可编程的空白石板(blank slates)演化,开发人员可以在那里实现自己的算法。不同种类的可编程着色器是控制GPU的主要手段。为了提高效率,管道的某些部分仍然是可配置的,不是可编程的,但是趋势是可编程性和灵活性。
GPU通过专注于一组(a narrow set of)可高度并行化的任务从而获得出色的速度。他们拥有专用于实现z缓冲区的定制芯片,可以快速访问纹理图像和其他缓冲区,例如,找到一个三角形所覆盖的那些像素。这些元素如何执行这些功能在23章中介绍。早期更重要的是要知道GPU如何为它的可编程着色器完成并行化。
3.3节解释了着色器的功能(how shaders function)。现在,你需要知道的是着色器的核心是一个小的处理器,用来做一些相对孤立的任务,比如把一个顶点从世界左边变换为屏幕坐标,或者是计算一个被三角形覆盖的像素的颜色。随着每帧数千或数百万三角形被发送到屏幕上,每秒有数十亿次的着色器调用,也就是说,着色器程序运行在单独的实例上。
首先,延迟是所有处理器都面临的问题。访问数据需要一些时间。考虑延迟的一个基本方法是,信息离处理器越远,就要等待越长时间。23.3节介绍延迟的更多细节。访问存储在内存芯片中的信息比访问本地寄存器信息需要花费更长时间。18.4.1节讨论内存访问的更多细节。关键点是等待获取数据意味着处理器处理器会被拖延(stalled),这会降低性能。
3.1 数据并行架构
不同的处理器框架采用不同的策略避免拖延(stalls)。CPU已针对各种数据结构和大型代码库进行了优化。CPU可以有多个处理器,但是每个处理器都以串行方式运行代码,SIMD向量处理是次要的例外。为了尽量减少延迟的影响,CPU的大部分芯片都由快速本地缓存组成,本地缓冲被接下来可能需要的数据所填充。CPU还通过使用诸如分支预判(branch prediction),指令重排(instruction reordering),寄存器重命名(register renaming),缓存预取(cache prefetching)等精巧技术来避免拖延(stalls)。
GPU采用不同的方法。GPU大部分芯片区域专用于大量的处理器,叫做着色器核心,通常有数千个。GPU是流处理器,其中依次处理相似数据的有序集。因为这种相似性(例如一组顶点或像素),GPU可以用大规模并行的方式来处理这些数据。另一个重要因素是这些调用尽可能独立,这样它们不需要邻近调用的信息并且不共享可写的内存位置。这个规则有时会被破坏以允许新的和有用的功能,但是这样的例外会以潜在的延迟作为代价,因为一个处理器获取会等待另一个处理器完成它的工作。
GPU针对吞吐量进行了优化,吞吐量被定义为数据的最大处理速率。然而,这种快速处理具有成本。更少的芯片区域专用于缓存和控制逻辑,每个着色器核心的延迟通常是大大高于CPU处理器遇到的情况。
假设一个网格物体已被光栅化,有两千个像素有要处理的片段(fragments);一个像素着色程序要被调用两千次。想象这里只有一个着色器处理器,世界上最弱的GPU。它开始为两千个片段的第一个执行着色程序。着色器处理器对寄存器中的值执行一些算数运算。寄存器是本地的访问很快,没有拖延(stall)发生。着色器处理器接着执行,例如执行到一个纹理访问;例如,对于一个给定的表面位置,程序需要知道应用到网格上的图片的像素颜色。纹理是一个完全独立的资源,不是像素程序本地内存的一部分,纹理访问参与进来。一次内存获取可以花费数百至数千个时钟周期,在那期间GPU处理器什么都不做。此时,着色器处理器会拖延(stall),等待纹理的颜色值返回。
为了使这个GPU有明显的改观,给每个前段的本地寄存器一点存储空间。现在,着色器处理器允许切换并执行另一个片段(两千个中的第二个),而不是拖延(stalling)在纹理获取。这个切换非常快,第一个和第二个片段什么都没受到影响,除了第一个片段上没有指令再执行。现在第二个片段被执行。和执行第一个时一样,一些算数函数被执行,然后再次遇到纹理获取。着色器核心现在切换到另一个片段,第三个。最终两三个片段全部以这种方式被执行。这时着色器处理器返回到第一个片段。这次纹理颜色被获取到并且可以使用,因此着色器程序可以继续执行。处理器用同样的方式处理直到另一个拖延(stall)执行的指令被遇到,或者程序完成。一个片段会话费更长的执行时间相比于着色器处理器专注于一个片段,但是所有片段的总执行时间大大的减少了。
在这个架构中,通过切换到另一个片段让GPU保持忙碌来隐藏延迟。GPU通过分离指令执行逻辑和数据来使这个设计更进一步。叫做单指令,多数据(SIMD),这个安排以固定的步数(lock-step)在固定数量的着色器程序上执行相同的命令。SIMD的优势是专用于处理数据和切换的硅(和电量)明显减少,相比于使用独立的逻辑和分配单元去运行每个程序。把我们两千个片段的例子转换成现代GPU术语,一个片段的每个像素着色器调用叫做一个线程。这个类型的线程不同于CPU线程。它由着色器输入值的一点内存,和着色器执行所需的任意寄存器空间组成。使用相同着色器程序的线程被打包成组,在NVIDIA中叫warps,AMD叫wavefronts。一个warps或wavefronts通过一些数量的GPU着色器核心被安排执行,从8到64的任何地方,使用SIMD处理。每个线程被映射到一个SIMD通道(lane)。
假设我们有两千个线程要执行。在NVIDIA GPU的Warps上包含32个线程。这产生了2000/32=62.5个warps,意味着分配了63个warps,一个warp一半是空的。warp的执行和我们单个GPU处理器的例子类似。着色器程序在所有32个处理器上以固定步数(lock-step)执行。当遇到内存获取,所有线程同时遇到,因为同样的指令在所有的线程上执行。获取示意这个warp的线程将拖延(stall),所有都在等待它们的(不同的)结果。这里不会拖延(stall),这个warp会为一个不同的32个线程的warp交换出去,换进来的warp然后被32个核心执行。这个交换和我们单一处理器系统一样快,因此当一个warp被换进或换出时,每个线程没有数据被触及(touched)。每个线程有它自己的寄存器,每个warp跟踪正在执行那条指令。换进一个新的warp只是将一组核心指向不同的一组线程去执行;这里没有其它开销。Warps执行或者换出知道所有都完成。看图3.1。
着我们这个简单的例子中一张纹理的内存获取延迟能导致warp交换出去。在实际中warps会因为更短的延迟被交换出去,因为交换的成本非常低。这里还有一些其它的技术用来优化执行,但是warp-swapping是所有GPU使用的主要的延迟隐藏(latency-hiding)机制。此过程的效率如何涉及多个因素。例如,如果这里只有少量的线程,然后只有少量的warps会被创建,从而导致延迟隐藏(latency hiding)有问题。
着色器程序的结构是影响效率的一个重要特征。一个主要因素是每个线程使用的寄存器数量。在我们的例子中我们假设可以一次将2000个线程全部驻留在GPU上。每个线程关联的着色器程序所需的寄存器越多,能驻留在GPU中的线程就更少,因此warps也更少。warps的短缺可能意味着挂起(stall)不能通过交换减轻。驻留的warps被叫做“飞行中”,驻留的数量叫做占用(occupancy)。高占用意味着这儿有许多可用与处理的warps,因此空闲处理器可能更少。占用率低通常会导致性能不佳。内存获取的频率同样影响需要多少延迟隐藏(latency hiding)。Lauritzen概述了占用被寄存器的数量和一个着色器使用共享内存的影响。Wronski讨论了理想的占用率如何根据一个着色器执行的操作类型而变化。
影响整体效率的另一个因素是动态分支,由"if"语句和循环造成。加入在一个着色器程序中遇到一条“if”语句,如果所有的线程求值采用相同的分支,warp可以继续而不用考虑其它分支。然而,如果一些线程,或者甚至只是一个线程,采用另一条路径,那么warp必须执行这两个分支,扔掉每个特定线程不需要的结果。这个问题叫做线程分歧(thread divergence),其中一些线程可能需要执行一个循环迭代或者执行一个"if"路径,而其它warp内的线程不需要,在此期间只能让这些不需要的线程处于空闲状态。
所有的GPU实现这种架构想法,导致系统有严格的限制但是每瓦特有大量的算力。明白系统如何运转(operates)将会帮助作为程序员的你更高效的使用它提供的算力。在后面的小节里我们讨论GPU如何实现渲染管道,可编程着色器如何运转(operate),和GPU每个阶段的演化和功能。
3.2 GPU管道概述
GPU实现概念的几何处理,光栅化,和像素处理管道阶段在第2章中描述。它们被分成几个硬件阶段有不同程度的可配置性和可编程性。图3.2显示了不同阶段颜色根据它们可编程和可配置的程度。注意这些物理阶段的拆分和第2章中介绍的功能阶段有点不一样。
我们这里描述GPU的逻辑模型,通过一组API暴露给程序员。就像18和23章中讨论的,这个逻辑管道的实现,物理模型,依赖于硬件供应商。一个在逻辑模型中的固定功能阶段或许通过在相邻的可编程阶段添加命令在GPU上执行。在管道中的一个程序或会被拆分成多个元素被独立的子单元(sub-units)执行,或者完全由一个单独的通道(pass)执行。逻辑模型可以帮助您推断会影响性能的原因,但是它应该不要误解为GPU实际实现管道的方式。
顶点着色器时一个完全可编程的阶段,用来实现几何处理阶段。几何着色器是一个完全可编程的阶段,在图元(点,线,和三角形)的顶点上操作。它可以用来执行每图元着色操作,销毁图元,或者创建新的图元。曲面细分(tessellation)阶段和几何着色阶段都是可选的,不是所有的GPU都支持它们,尤其是在移动设备上。
裁剪,三角形设置(triangle setup),和三角形遍历(triangle traversal)通过固定功能硬件实现。屏幕映射受窗口和视口设置的影响,内部形成了一个简单的缩放和重新定位。像素着色器阶段是完全可编程的。尽管合并(merger)阶段不是可编程的,它是高度可配置的并且可以设置执行许多操作。它实现"合并"(merging)的功能阶段,负责修改颜色,z-buffer,混合,模板和其它输出相关的缓冲区。像素着色器的执行与合并阶段形成了概念的像素处理阶段在第2章介绍。
随着时间的流逝,GPU管道已从硬编码操作演变为增强灵活性和控制力。可编程着色器阶段的介绍是这一演变过程中最重要的一步。下一节将介绍各个可编程阶段共有的功能。
3.3 可编程着色器阶段
现代着色器程序使用统一的着色器设计。这意味着顶点,像素,几何,和曲面细分相关的着色器共享一个通用的编程模型。在内部它们有相同的指令集架构(ISA)。实现此模型的处理器在DirectX中叫做通用着色器核心,有这种核心的GPU被称为有统一着色器架构。这种类型的架构背后的想法是着色器处理器可用于多种角色,GPU可以根据需要来分配这些。例如,一组由小三角形组成的网格比由两个三角形组成的大正方形需要更多的顶点着色器处理。一种具有单独的顶点和像素着色器核心池的GPU意味着为了让所有核心忙碌的理想工作分配是僵硬地提前决定的。在统一着色器核心中,GPU可以决定如何平衡此负载。
描述整个着色器编程模型大大超出了本书的范围,并且有许多文档,书,和网站已经这么做了。着色器使用类似C(C-like)的着色语言进行编程,比如DirectX的高级着色语言(HLSL)和OpenGL的着色语言(GLSL)。DirectX的HLSL可以被编译成虚拟机字节码(virtual machine bytecode),也叫做中间语言(IL或DXIL),以提供硬件独立性。中间表示也可以允许着色器程序离线编译和存储。这种中间语言由驱动程序转换为特定GPU的ISA。控制台编程通常避免中间语言步骤,因为那时系统中只有一个ISA。
基本数据类型是32位单精度浮点数的标量和向量,尽管向量只是着色器代码的一部分,在硬件中并不支持就像
上面概述的。现代GPU同样原生地支持32位整数和64位浮点数。浮点数向量通常包含数据如位置(xyzw),法线,矩阵行,颜色(rgba),或者纹理坐标(uvwq)。整数通常用于表示计数,索引,或者位掩码。聚合数据类型例如结构,数组,和矩阵同样也支持。
绘图调用会调用图形API来绘制一组图元,因此导致图形管道来执行和运行其着色器。每个可编程着色器阶段有两种类型的输入:统一输入(uniform inputs),这些值在整个绘图调用中保持不变(但两次绘图调用间可以改变),和可变输入(varying inputs),从三角形顶点或光栅化过来的数据。例如,像素着色器或许提供一个光源的颜色作为统一(uniform)值,三角形表面的位置每像素改变因此是可变值(varying)。一个纹理是一种特殊类型的统一(uniform)输入,曾经总是应用于表面的一个颜色图像,但是现在在这里可以被认为是任意大数据的数组。
底层的虚拟机为不同类型的输入和输出提供特殊的寄存器。统一值(uniforms)可用的常量寄存器的数量远大于可变(varying)值或输出可用的寄存器数量。这是因为可变(varying)输入和输出需要为每个顶点和像素单独存放,因此对于需要多少个有一个自然的限制。统一(uniform)输入只被存储一次并且被绘图调用内的所有顶点和像素重用。虚拟机还具有通用临时寄存器,用于暂存空间。可以使用临时寄存器中的整数值对所有类型的寄存器进行数组索引。着色器虚拟机的输入和输出如图3.3所示。
图形计算中常见的运算可在现代GPU中高效的执行。着色语言提供(expose)了这些操作中最常见的(例如加和乘)通过运算符例如*和+。其余的通过固有的函数提供,即,atan(), sqrt(), log(), 和一些其它的,为GPU优化的。还存在一些更复杂运算的函数,比如向量法线和反射,叉乘,和矩阵转置和行列式的计算。
术语流程控制指的是使用分支指令改变代码执行流程。流程控制相关的指令用来实现高级语言构造例如“if”和“case”语句,还有不同类型的循环语句。着色器支持两种类型的流程控制。静态流程控制分支基于统一(uniform)输入的值。这意味着代码流在整个绘制调用中是不变的。静态流程控制的主要好处是允许可以在各种不同情况下使用的同一个着色器(即,不同数量的光源)。这里没有线程分歧(thread divergence),因为所有的调用都采用相同的代码路径。动态流程控制基于可变(varying)输入的值,意味着每个片段可以不同地执行代码。这比静态流程控制强大的多但会降低性能,尤其是如果代码流在着色器调用间不规则地改变。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 02:04 , Processed in 0.067113 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表