Microsoft DirectX* 12 中资源绑定的性能考虑因素

作者:Confetti CEO Wolfgang Engel

随着 7 月 29 日发布 Windows* 10,以及第六代英特尔® 酷睿™ 处理器产品家族(代号 Skylake),我们现在能够更深入地利用专为英特尔® 平台开发的资源绑定。

上一篇文章《Microsoft DirectX* 12 中的资源绑定介绍》介绍了 DirectX 12 中的全新资源绑定方法,并得出一个结论,即有了这些丰富选择,我们只需要针对目标 GPU、资源类型及其更新频率选择最合适的绑定机制即可。

本文介绍了如何挑选不同的资源绑定机制,在特定英特尔 GPU 上高效运行应用。

必备工具

如要使用 DirectX 12 开发游戏,您需要以下工具:

  • Windows 10
  • Visual Studio* 2013 或更高版本
  • 带有 Visual Studio 的 DirectX 12 SDK
  • 支持 DirectX 12 的 GPU 和驱动程序

概述

描述符是以 GPU 特定的 opaque 格式描述 GPU 对象的数据块。 DirectX 12 可提供以下描述符,以前在 DirectX 11 中名为“资源视图”:

  • 常量缓冲视图 (CBV)
  • 着色器资源视图 (SRV)
  • 无序访问视图 (UAV)
  • 取样器视图 (SV)
  • 渲染器目标视图 (RTV)
  • 深度模板视图 (DSV)
  • 其他

这些描述符或资源视图可被 GPU 前端当作一个结构(又称“块”)来使用。 这些描述符大约为 32–64 字节,存放的信息包括纹理维度、格式和布局。

描述符存储在描述符堆中,在内存中呈现为一个结构序列。

一个描述符表存放了该描述堆的偏移量。 它可通过向连续的描述符提供一个根签名,将其映射到着色器插槽中。 该根签名还可存放根常量、根描述符和静态采样器。

Descriptors, descriptor heap, descriptor tables, root signature

图 1。 描述符、描述符堆、描述符表、根签名。

图 1 展示了描述符、描述符堆、描述符表和根签名之间的关系。

图 1 描述的代码如下:

// the init function sets the shader registers
// parameters: type of descriptor, num of descriptors, base shader register
// the first descriptor table entry in the root signature in
// image 1 sets shader registers t1, b1, t4, t5
// performance: order from most frequent to least frequent used
D3D12_DESCRIPTOR_RANGE Param0Ranges[3]; 
Param0Ranges[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 1); // t1 Param0Ranges[1].Init(D3D12_DESCRIPTOR_RANGE_CBV, 1, 1); // b1 Param0Ranges[2].Init(D3D12_DESCRIPTOR_RANGE_SRV, 2, 4); // t4-t5  

// the second descriptor table entry in the root signature
// in image 1 sets shader registers u0 and b2
D3D12_DESCRIPTOR_RANGE Param1Ranges[2]; Param1Ranges[0].Init(D3D12_DESCRIPTOR_RANGE_UAV, 1, 0); // u0 Param1Ranges[1].Init(D3D12_DESCRIPTOR_RANGE_CBV, 1, 2); // b2  

// set the descriptor tables in the root signature
// parameters: number of descriptor ranges, descriptor ranges, visibility
// visibility to all stages allows sharing binding tables
// with all types of shaders
D3D12_ROOT_PARAMETER Param[4]; 
Param[0].InitAsDescriptorTable(3, Param0Ranges, D3D12_SHADER_VISIBILITY_ALL); 
Param[1].InitAsDescriptorTable(2, Param1Ranges, D3D12_SHADER_VISIBILITY_ALL); // root descriptor
Param[2].InitAsShaderResourceView(1, 0); // t0
// root constants
Param[3].InitAsConstants(4, 0); // b0 (4x32-bit constants)

// writing into the command list
cmdList->SetGraphicsRootDescriptorTable(0, [srvGPUHandle]); 
cmdList->SetGraphicsRootDescriptorTable(1, [uavGPUHandle]);
cmdList->SetGraphicsRootConstantBufferView(2, [srvCPUHandle]);
cmdList->SetGraphicsRoot32BitConstants(3, {1,3,3,7}, 0, 4);

上述的源代码设置了一个根签名,其中包括两个描述符表、一个根描述符和一个根常量。 该代码还显示,根常量直接使用 SetGraphicsRoot32bitConstants 调用获取,没有中间层级。 它们直接路由至着色器寄存器;没有实际出现常量缓存区、常量缓存区描述符或绑定。 根描述符只有一个层级,因为它们在内存中存储了一个指针(指针 -> 内存),描述符表有两个层级(描述符表 -> 描述符 -> 内存)。

描述符存放在不同的堆中,具体取决于其类型,如 SV 和 CBV/SRV/UAV。 这是因为在不同的硬件平台上,不同类型的描述符的大小普遍不一致。 对于每种类型的描述符堆,应该仅分配了一个堆,因为更换堆的代价太昂贵。

一般而言,DirectX 12 可提前分配 100 万个描述符,足够整个游戏使用。 虽然以前的 DirectX 版本根据自己的情况在驱动程序中处理分配,但是使用 DirectX 12 可以避免在运行时进行分配。 这意味着,描述符的初次分配可从性能“等式”中去除。

: 借助第三代英特尔® 酷睿™ 处理器(代号 Ivy Bridge)或第四代英特尔® 酷睿™ 处理器产品家族(代号 Haswell)以及 DirectX 11 和 Windows Display Driver Model (WDDM) 1.x 版,可使用页表映射操作根据在命令缓冲区中引用的资源将资源动态映射至内存。 通过这种方式,可以避免数据复制。 动态映射非常重要,因为这些架构仅可向 GPU 提供 2GB 的内存(英特尔® 至强™ 处理器 E3-1200 v4 产品家族(代号 Broadwell)可提供更多内存)。
借助 DirectX 12 和 WDDM 2.x 版,不必再将资源重新映射至 GPU 虚拟地址空间,因为在创建时需要从资源中分配出一个静态虚拟地址,因此在创建后,资源的虚拟地址便不能再更改了。 即使资源从 GPU 内存中“驱逐”出去,它还可以保留自己的虚拟地址,以便其以后再次存放时使用。
因此,Ivy Bridge/Haswell 的可用总内存(2GB)可能会成为限制因素。

如在前一篇文章中所述,一款应用的完美成功应包括所有类型的绑定:根常量、根描述符、发起绘制调用时便于动态收集描述符的描述符表以及大描述符表的动态索引功能。

不同的硬件架构会表现出不同的性能权衡,有的使用根常量集和根描述符较多,有的使用描述符表较多。 因此,可能需要根据硬件目标平台调整根参数和描述符表之间的比率。

预期变化模式

如要了解哪些类型的变更需要额外成本,我们需要先分析一般情况下游戏引擎如何变更数据、描述符、描述符表和根签名。

我们首先来看一下常量数据。 通常,大部分的游戏引擎将所有常量数据存储在“系统内存”中。 游戏引擎更改 CPU 可访问的内存中的数据,然后在处理该帧期间,整个常量数据块复制/映射到 GPU 内存,然后 GPU 通过一个常量缓存视图或根描述符读取数据。

如果常量数据通过 SetGraphicsRoot32BitConstants() 作为根常量提供,那么根描述符中的条目不会改变,但是数据可能会改变。 如果通过使用 CBV == 描述符,再使用描述符表的方式提供,那么描述符不会改变,数据可能会改变。

以防我们需要多个常量缓冲视图(比如为便于双重或三重缓冲渲染),根签名中每帧的 CBV 或描述符可能都需要改变。

对于纹理数据,预计纹理在启动时会分配到 GPU 内存中。 然后将会创建一个 SV == 描述符,并存储到描述符表或静态采样器中,再被引用到根描述符中。 此后,数据和描述符或静态样本不会再变化。

对于不断变化的纹理或缓冲数据等动态数据(例如带有渲染的本地化文本的纹理、动画顶点缓冲区或程序生成的网格),我们分配一个渲染目标或缓冲区,提供一个 RTV 或 UAV (描述符),然后这些描述符从此开始就不会再变化。 渲染器目标或缓存区中的数据可能会变化。

以防我们需要多个渲染目标或缓冲区(比如为便于双重或三重缓冲渲染),根签名中每帧的描述符可能都需要改变。

对于以下讨论,如果执行以下操作,对于绑定资源需要进行改变:

  • 更改或更换描述符表中的描述符,如上述的 CBV、RTV 或 UAV
  • 更改根签名中的条目

采用 Haswell/Broadwell 的描述符表中的描述符

在基于 Haswell/Broadwell 的平台上,更改根签名中的一个描述符表的成本相当于更换所有的描述符表。 更改一个参数意味着硬件需要复制目前所有的参数。 根签名中的根参数数量是更改子集时硬件需要复制的数据量。

注: 对于 DirectX 12 中的其他内存类型(描述符堆、缓存资源等),并不是由硬件实现版本化。

换言之,更换所有参数的成本基本相当于更换一个参数(参见 [Lauritzen] 和 [MSDN])。 不做任何更改仍然是成本最小的方式,但是没有太大的价值。

注: 其他的硬件,比如需要在较快或较慢(溢出)的根参数存储间划分,只需要复制参数发生变化的内存区域即可,包括较快的区域和溢出区域。

在 Haswell/Broadwell 上,硬件中的绑定表较小可能会让更改描述符表额外支付成本。

这些硬件平台上的描述符表使用“绑定表”硬件。 每个绑定表条目都是一个单独的 DWORD,可当作描述符堆的偏移量。 64 KB 的环状物可存储 16,384 个绑定表条目。

换言之,每个绘制调用使用的内存量取决于在描述符表中建立索引,然后通过根签名引用的描述符的总个数。

为了防止我们在绑定表条目中将 64 KB 内存用光,驱动程序将会再分配 64 KB 的绑定表。 这些表之间的切换可能会导致管线出现停顿,如图 2 所示。

Pipeline stall (courtesy of Andrew Lauritzen)

图 2管线停顿(Andrew Lauritzen 提供)。

例如,一个根签名会在一个描述符表中引用 64 个描述符。 停顿每隔 16,384 / 64 = 256 次绘制调用出现一次。

由于更改根签名的成本较小,建议在描述符表中使用多个包含较少描述符的根签名,而非在描述符表中使用包含更多描述符的根签名。

因此,建议在 Haswell/Broadwell 上,在描述符表中尽可能少地使用描述符。

这对于渲染器设计意味着什么? 使用更多包含较少描述符的描述符表,因此更多的根签名应该会增加管线状态对象的数量,因为根签名的数量增加,PSO 的数量就需要增加,因为二者之间是一对一的关系。

有更多管线状态对象会带来更多的着色器,在这种情况下,着色器应该是更加专业的,而非更长的能够提供多种特性的,这是通用建议。
 

Haswell/Broadwell 上的根常量/描述符

上文提到更改一个描述符表的成本相当于全部更改,与此相同,更改一个根常量或根描述符也相当于全部更改(参见 [Lauritzen])。

根常量使用 “push constants” 实施,该缓存支持硬件用来预先安装执行单元 (EU) 寄存器。 由于 EU 线程启动时值立即可用,所以从性能角度优劣来讲,最好将常量数据存储为根常量,而非描述符表。

根描述符同样实施为 “push constants”。 它们支持作为常量传递到着色器的指针,可通过一般的内存路径读取数据。

Haswell/Broadwell 上的描述符表与根常量/描述符

我们已经了解了描述符表、根常量和描述符实施的方式,现在我们可以回答本文的主要问题:二者之间是否有优劣之分? 由于绑定表硬件的尺寸有限并且跨过这一限制可能会导致停顿,所以在 Haswell/Broadwell 硬件上更改根常量和根描述符成本会更低,因为它们不使用绑定表硬件。 对于根描述符和根常量,尤其建议此方法,以防数据在每个绘制调用时都会发生变化。

Haswell/Broadwell 上的静态采样器

如上一篇文章中的描述,可以使用 HLSL 根签名语言在根签名或直接在着色器中定义采样器。 它们被称作静态采样器。

在 Haswell/Broadwell 硬件上,驱动程序将在常规采样器堆中使用静态采样器。 这相当于手动将其放到描述符中。 其他的硬件在着色器寄存器中实施采样器,以便静态采样器能够直接编译到着色器中。

一般情况下,静态采样器在许多平台上都是有优势的,因此使用它并没有什么坏处。 在 Haswell/Broadwell 硬件上,还可以增加描述符表中的描述符数量,管线停顿可能更频繁地发生,因为描述符表硬件仅提供了 16,384 个插槽。

以下是 HLSL 中的静态采样器语法:

StaticSampler( sReg,
               [ filter = FILTER_ANISOTROPIC, 
               addressU = TEXTURE_ADDRESS_WRAP,
               addressV = TEXTURE_ADDRESS_WRAP,
               addressW = TEXTURE_ADDRESS_WRAP,
               mipLODBias = 0.f,     maxAnisotropy = 16,
               comparisonFunc = COMPARISON_LESS_EQUAL,
               borderColor = STATIC_BORDER_COLOR_OPAQUE_WHITE,
               minLOD = 0.f, maxLOD = 3.402823466e+38f,
               space = 0, visibility = SHADER_VISIBILITY_ALL ])

多数参数都是不言自明的,因为它们与 C++ 层的使用是相同的。 主要区别在于边框颜色:在 C++ 上,它可以提供完整的色彩范围,而 HLSL 层仅限于不透明的白色/黑色和透明的黑色。 以下是静态着色器的一个示例:

StaticSampler(s4, filter=FILTER_MIN_MAG_MIP_LINEAR)

Skylake

Skylake 支持在一个描述符表中对整个描述符堆(约 100 万个资源)动态创建索引。 这意味着,一个描述符表足够为所有可用的描述符堆内存创建索引。

与以前的架构相比,无需再经常更改根签名中的描述符表条目。 这也意味着可以减少使用根签名的数量。 显然,不同的材料需要不同的着色器,因而需要不同的 PSO。 但是这些 PSO 可以引用相同的根签名。

现代渲染引擎使用的着色器比 DirectX 9 和 11 少,因而可以避免更改着色器及相关状态的成本,根签名数量的减少以及随之导致的 PSO 数量的减少是较为有利的,可为任何硬件平台带来性能提升。

结论

对于 Haswell/Broadwell 和 Skylake 而言,开发高性能 DirectX 12 的建议需要视底层的平台而定。 虽然对于 Haswell/Broadwell 而言,应该尽可能在描述符表中使用较少的描述符,但是对于 Skylake,建议增加描述符的数量,并减少描述符表的数量。

为了实现最佳性能,应用编程人员可以在启动器件检查硬件的类型,然后选择最有效的资源绑定模式。 (以下连接中提供了一个 GPU 检查示例,展示了如何检查不同的英特尔® 硬件架构:https://software.intel.com/zh-cn/articles/gpu-detect-sample/)资源绑定模式的选择将会影响到如何编写系统着色器。

关于作者

Wolfgang 是 Confetti 的首席执行官。 Confetti 是电子游戏和电影行业的领先提供商,致力于提供先进的实时图形研究和服务,并为整个行业带来绝佳的创意。 在与伙伴联合创办 Confetti 之前,Wolfgang 曾在 Rockstar 的核心技术事业部 RAGE 中工作过四年的时间,并担任首席图形编程师。 他是 ShaderXGPU Pro 系列丛书的作者和编辑,是微软公司的 MVP,编写过许多有关实时渲染的书籍和文章,并且经常全球多家网站和会议撰稿。 他编辑的书籍《ShaderX4》曾获得 2006 年的游戏开发人员前线大奖。 Wolfgang 在行业内的多个咨询委员会任职;其中包括针对 DirectX 12 的微软图形咨询委员会。 他积极参与多个未来标准的建立,致力于推动游戏行业的发展。 您还可以在 Twitter 上了解他,帐户名为 @wolfgangengel。 Confetti 的网站是:www.conffx.com

致谢

衷心感谢本文的评稿人:

  • Andrew Lauritzen
  • Robin Green
  • Michal Valient
  • Dean Calver
  • Juul Joosten
  • Michal Drobot

参考资料和相关链接

**在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下才能得到优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能, 上述任何要素的变动都有可能导致测试结果的变化。 请参考其他信息及性能测试(包括结合其他产品使用时的运行性能)以对目标产品进行全面评估。

有关编译器优化的更完整信息,请参阅优化通知