nBody DirectX® 12 示例 (异步计算版本)
这是 Microsoft D3D12nBodyGravity 示例的一个稍作修改的版本,该示例演示了如何使用异步计算着色器(多引擎)来模拟 n 体引力系统。
今天我们将探讨异步计算如何帮助您充分利用 GPU。我将基于 Microsoft 的 nBodyGravity 示例来详细解释——但首先让我们从一些背景信息开始!
异步计算是 GPU 的一个新概念,但您可能在 CPU 上非常熟悉它,通常称为 SMT(同时多线程)。这到底意味着什么?一般来说,您的 CPU 拥有比单个线程所能使用的更多的执行单元。例如,CPU 可能每时钟周期能执行 4 个操作,但如果您的线程正在等待内存访问完成,执行单元就会空闲。通过发出第二个线程的指令,很有可能填补这些空白并获得更高的吞吐量。
GPU 在这方面非常相似。您有不同的单元,如光栅器、计算核心和混合单元,每个单元都可能成为给定绘制调用的瓶颈。GPU 的管道比 CPU 深得多,以避免这种情况发生,但仍会有一些单元空闲的情况。一个很好的例子是阴影贴图渲染,它通常对光栅器或三角形吞吐量要求很高,但大部分计算单元都空闲。
借助 Direct3D® 12 和 Vulkan™,开发人员获得了一个工具来表达哪些计算可以并行发生——即图形和计算队列。图形队列可以使用所有执行资源——复制引擎、计算核心、光栅器等,而计算队列只能使用计算核心。通过在队列上调度任务,开发人员向 GPU 调度程序清楚地表明这些任务可以独立执行,并且可能与其他队列上的任务并发执行。
队列之间的同步必须使用栅栏手动完成。例如,nBodyGravity 演示的默认版本会这样做,以定期渲染模拟结果。每进行几次模拟步骤,它就会将计算队列与图形队列同步,渲染结果,一旦结果显示出来,就继续模拟。在 GPU 进行渲染时,计算队列会空闲。我们可以在下面的 GPUView 跟踪中看到这一点。

我们可以看到计算队列执行多个数据包,然后在图形队列忙碌时暂停,然后恢复工作。这导致 GPU 的总忙碌时间仅为 85%,因此我们浪费了很多性能。让我们看看如何重构代码,让图形和计算并行执行。
那么我们如何才能改进这一点呢?在我 修改的 nBodyGravity 示例中,我做的关键改变是双缓冲模拟。也就是说,下一帧的模拟在当前帧渲染时运行。只要渲染时间明显短于模拟时间,计算队列就会一直忙于模拟,而图形队列会尽快准备好渲染结果。同步仍然与原始示例类似——使用栅栏在模拟完成后触发渲染——但主要区别在于模拟数据在渲染之前被复制到“渲染内存”中。然后立即排队下一个模拟步骤,并在前一帧的渲染作业上设置栅栏。需要栅栏来确保模拟不会进行得太远。通过这些小改动,我们就得到了以下行为:
我们可以在 GPUView 中验证这一点。

在上面的跟踪中,我们可以看到图形队列大约有 20% 的时间处于忙碌状态,而计算队列则 100% 的时间都被使用。总体而言,GPU 的忙碌时间是总时间的“125%”——因为我们有不止一个队列在忙碌。我们使用光栅器和混合单元进行渲染(在模拟期间它们是空闲的)。这使得我们可以让 GPU 的更多单元保持忙碌,并提高相对于顺序执行的性能,在顺序执行中,在渲染粒子时计算单元大部分是空闲的。您可以通过将 AsynchronousComputeEnabled = false 来自行验证这一点,这将完全移除同步并将所有内容按顺序提交到图形队列。在 AMD Radeon Fury X 上,未启用异步计算的每帧性能约为 8 毫秒。启用异步计算后,我们将此降低到 7 毫秒——提高了 15%。虽然这不算什么大事,但这种有限的收益是由于图形工作负载也需要一些计算。由于我们在 25% 的重叠上获得了 15% 的改进,我们可以估计大约一半的图形工作负载实际上是计算。当任务具有广泛不同的特征时,可以获得更高的增益。例如,在渲染阴影贴图时运行环境光遮蔽内核可能完全是免费的,因为它们会占用不同的执行资源。
这个示例就到这里!您可以在 GitHub 上找到源代码,这样您就可以确切地了解双缓冲和同步是如何更改的。