返回 登录
2

使用英特尔® SPMD 程序编译器实现游戏 CPU 的矢量化

简介

基于 LLVM*英特尔® SPMD 程序编译器 (在之前的文档中通常被称作 ISPC)并不是 Gnu* 编译器套装 (GCC) 或 Microsoft* C++ 编译器的替代品;它更类似于面向 CPU 的着色器编译器,可生成适用多种指令集的矢量指令,如英特尔® SIMD 流指令扩展 2(英特尔® SSE2)、英特尔® SIMD 流指令扩展 4(英特尔® SSE4)、英特尔® 高级矢量扩展指令集(英特尔® AVX)、英特尔® AVX2 等。输入基于 C 的着色器或内核,输出预编译对象文件,您的应用中将包含一个头文件。通过使用少量关键字,编译器便可以得到关于在 CPU 矢量单元上分配工作的明确指示。

如果开发人员选择将内联函数直接编写到代码库,显式矢量化便可提供更高性能,但是这样做极为复杂,维护成本较高。英特尔 SPMD 程序编译器 内核使用高级语言编写而成,因此开发成本较低。相比英特尔 SSE4 等最常见的指令集,它还可以轻松支持多个指令集,为运行代码的 CPU 提供最佳性能。

本文的目的并不是教会读者如何编写英特尔 SPMD 程序编译器内核;本文简单介绍了如何将英特尔 SPMD 程序编译器插入 Microsoft Visual Studio* 解决方案,提供了关于如何将简单的高级着色语言* (HLSL*) 计算着色器导入英特尔 SPMD 程序编译器内核的指导。如欲获取关于英特尔 SPMD 程序编译器更详细的概述,请参阅在线文档

本文提供的示例代码基于 Microsoft DirectX* 12 n 体示例的修改版本,为了支持英特尔 SPMD 程序编译器矢量化计算内核,已导入该版本。本文的目的并不是显示 GPU 的性能增量,而是显示从标准标量 CPU 代码迁移至矢量化 CPU 代码实现的性能提升。

由于本应用原始示例中的 CPU 负载非常小,显然不能代表一个游戏,但是本应用显示了在多个 CPU 内核上使用矢量单元可能会提升性能。图片描述
图 1. 经过修改的 n 体重力示例截屏

原始 DirectX* 12 n 体重力示例

开始移植英特尔 SPMD 程序编译器之前,了解原始示例及其目的非常有帮助。编写 DirectX 12 n 体重力示例是为了介绍如何使用 DirectX 12 中的独立计算引擎执行异步计算,也就是在 GPU 上并行执行粒子更新与粒子渲染。示例生成 10,000 个粒子,逐帧更新与渲染粒子。更新包括每个粒子与其他粒子的交互,每次模拟 tick 生成 100,000,000 个交互。

HLSL 计算着色器将计算线程映射至每个粒子,以执行更新。双缓冲粒子数据,因此,对于每一帧,GPU 从缓冲 1 开始渲染,异步更新缓冲 2,然后翻转缓冲,为下一帧做准备。

就是这么简单。不仅简单,它还是英特尔 SPMD 程序编译器移植的绝佳选择,因为异步计算任务适用于出色地运行于 CPU;代码和引擎可在并发执行路径中执行计算。通过将某些负载迁移至多数情况下未被充分利用的 CPU,GPU 可以更快地完成帧,或者完成更多工作,同时充分利用 CPU。

移植到英特尔® SPMD 程序编译器

建议首先从 HLSL 移植到标量 C/C++。这样确保了算法的正确性,生成了正确的结果,正确地与其它应用交互,以及恰当处理多个线程(如果适用)。听起来轻而易举,但是需要注意以下几点:

  1. 如何在 GPU 和 CPU 之间共享内存。
  2. 如何同步 GPU 和 CPU。
  3. 如何面向单指令/多数据 (SIMD) 和多线程划分任务。
  4. 将 HLSL 代码移植到标量 C。
  5. 将标量 C 移植到英特尔 SPMD 程序编译器内核。

某些操作相对简单。

共享内存

我们知道需要在 CPU 和 GPU 之间共享内存,但是如何共享?幸运的是,DirectX 12 提供一些选项,如将 GPU 缓冲映射到 CPU 内存等。为了简化示例,最大程度减少代码变更,我们重新使用了用于初始化 GPU 粒子缓冲的粒子上传临时缓冲,并创建了面向 GPU 访问的双缓冲 CPU 副本。使用模式成为:

  • 在 CPU 中更新 CPU 可访问的粒子缓冲。
  • 使用原始上传临时缓冲调用 DirectX 12 助手 UpdateSubresources,GPU 粒子缓冲作为目的地。
  • 绑定 GPU 粒子缓冲并渲染。

同步

如果原始异步计算内核已经有一个用于配置计算和渲染之间交互的 DirectX 12 栅栏对象,同步将自然发生,重新利用它通知渲染引擎副本已完成。

划分工作

为了划分工作,我们应首先考虑 GPU 如何划分工作,因为这可能同样适用于 CPU。计算着色器通过两种方式控制工作的划分。首先是调度大小,指的是录制命令流时传输至 API 调用的大小。描述了运行的工作组的数量和维度。第二个是本地工作组的数量和维度,该工作组被硬编码为着色器。本地工作组的每个项目可视作一个工作线程,如果使用了共享内存,每个线程可与工作组中的其他线程共享信息。


查看全文


了解更多相关内容,请关注CSDN英特尔开发专区

评论