写点什么

遇见 C++ AMP:在 GPU 上做并行计算

  • 2012-08-15
  • 本文字数:4604 字

    阅读完需:约 15 分钟

从 CPU 到 GPU

《遇见C++ PPL:C++ 的并行和异步》里,我们介绍了如何使用 C++ PPL 在 CPU 上做并行计算,这次,我们会把舞台换成 GPU,介绍如何使用 C++ AMP 在上面做并行计算。

为什么选择在 GPU 上做并行计算呢?现在的多核 CPU 一般都是双核或四核的,如果把超线程技术考虑进来,可以把它们看作四个或八个逻辑核,但现在的 GPU 动则就上百个核,比如中端的 NVIDIA GTX 560 SE 就有 288 个核,顶级的 NVIDIA GTX 690 更有多达 3072 个核,这些超多核(many-core)GPU 非常适合大规模并行计算。

接下来,我们将会在《遇见C++ PPL:C++ 的并行和异步》的基础上,对并行计算正弦值的代码进行一番改造,使之可以在GPU 上运行。如果你没读过那篇文章,我建议你先去读一读它的第一节。此外,本文也假设你对C++ Lambda 有所了解,否则,我建议你先去读一读《遇见C++ Lambda》

并行计算正弦值

首先,包含/ 引用相关的头文件/ 命名空间,如代码1 所示。amp.h 是C++ AMP 的头文件,包含了相关的函数和类,它们位于concurrency 命名空间之内。amp_math.h 包含了常用的数学函数,如sin 函数, concurrency::fast_math 命名空间里的函数只支持单精度浮点数,而 concurrency::precise_math 命名空间里的函数则对单精度浮点数和双精度浮点数均提供支持。

代码 1

把浮点数的类型从 double 改成 float,如代码 2 所示,这样做是因为并非所有 GPU 都支持双精度浮点数的运算。另外,std 和 concurrency 两个命名空间都有一个 array 类,为了消除歧义,我们需要在 array 前面加上“std::”前缀,以便告知编译器我们使用的是 STL 的 array 类。

代码 2

接着,创建一个 array_view 对象,把前面创建的 array 对象包装起来,如代码 3 所示。array_view 对象只是一个包装器,本身不能包含任何数据,必须和真正的容器搭配使用,如 C 风格的数组、STL 的 array 对象或 vector 对象。当我们创建 array_view 对象时,需要通过类型参数指定 array_view 对象里的元素的类型以及它的维度,并通过构造函数的参数指定对应维度的长度以及包含实际数据的容器。

代码 3

代码 3 创建了一个一维的 array_view 对象,这个维度的长度和前面的 array 对象的长度一样,这个包装看起来有点多余,为什么要这样做?这是因为在 GPU 上运行的代码无法直接访问系统内存里的数据,需要 array_view 对象出来充当一个桥梁的角色,使得在 GPU 上运行的代码可以通过它间接访问系统内存里的数据。事实上,在 GPU 上运行的代码访问的并非系统内存里的数据,而是复制到显存的副本,而负责把这些数据从系统内存复制到显存的正是 array_view 对象,这个过程是自动的,无需我们干预。

有了前面这些准备,我们就可以着手编写在 GPU 上运行的代码了,如代码 4 所示。 parallel_for_each 函数可以看作 C++ AMP 的入口点,我们通过 extent 对象告诉它创建多少个 GPU 线程,通过 Lambda 告诉它这些 GPU 线程运行什么代码,我们通常把这个代码称作 Kernel。

代码 4

我们希望每个 GPU 线程可以完成和结果集里的某个元素对应的一组操作,比如说,我们需要计算 10 个浮点数的正弦值,那么,我们希望创建 10 个 GPU 线程,每个线程依次完成读取浮点数、计算正弦值和保存正弦值三个操作。但是,每个 GPU 线程运行的代码都是一样的,如何区分不同的 GPU 线程,并定位需要处理的数据呢?

这个时候就轮到 index 对象出场了,我们的 array_view 对象是一维的,因此 index 对象的类型是 index<1>,这个维度的长度是 10,因此将会产生从 0 到 9 的 10 个 index 对象,每个 GPU 线程对应其中一个 index 对象。这个 index 对象将会通过 Lambda 的参数传给我们,而我们将会在 Kernel 里通过这个 index 对象找到当前 GPU 线程需要处理的数据。

既然 Lambda 的参数只传递 index 对象,那 Kernel 又是如何与外界交换数据的呢?我们可以通过闭包捕获当前上下文的变量,这使我们可以灵活地操作多个数据源和结果集,因此没有必要提供返回值。从这个角度来看,C++ AMP 的 parallel_for_each 函数在用法上类似于 C++ PPL 的 parallel_for 函数,如代码 5 所示,我们传给前者的 extent 对象代替了我们传给后者的起止索引值。

代码 5

那么,Kernel 右边的 restrict(amp) 修饰符又是怎么一回事呢?Kernel 最终是在 GPU 上运行的,不管以什么样的形式,restrict(amp) 修饰符正是用来告诉编译器这点的。当编译器看到 restrict(amp) 修饰符时,它会检查 Kernel 是否使用了不支持的语言特性,如果有,编译过程中止,并列出错误,否则,Kernel 会被编译成 HLSL ,并交给 DirectCompute 运行。Kernel 可以调用其他函数,但这些函数必须添加 restrict(amp) 修饰符,比如代码 4 的 sin 函数

计算完毕之后,我们可以通过一个 for 循环输出 array_view 对象的数据,如代码 6 所示。当我们在 CPU 上首次通过索引器访问 array_view 对象时,它会把数据从显存复制回系统内存,这个过程是自动的,无需我们干预。

代码 6

哇,不知不觉已经讲了这么多,其实,使用 C++ AMP 一般只涉及到以下三步:

  1. 创建 array_view 对象。
  2. 调用 parallel_for_each 函数。
  3. 通过 array_view 对象访问计算结果。 其他的事情,如显存的分配和释放、GPU 线程的规划和管理,C++ AMP 会帮我们处理的。

并行计算矩阵之和

上一节我们通过一个简单的示例了解 C++ AMP 的使用步骤,接下来我们将会通过另一个示例深入了解 array_view、extent 和 index 在二维场景里的用法。

假设我们现在要计算两个 100 x 100 的矩阵之和,首先定义矩阵的行和列,然后通过 create_matrix 函数创建两个 vector 对象,接着创建一个 vector 对象用于存放矩阵之和,如代码 7 所示。

代码 7

create_matrix 函数的实现很简单,它接受矩阵的总容量(行和列之积)作为参数,然后创建并返回一个包含 100 以内的随机数的 vector 对象,如代码 8 所示。

代码 8

值得提醒的是,当 create_matrix 函数执行“return matrix;”时,会把 vector 对象拷贝到一个临时对象,并把这个临时对象返回给调用方,而原来的 vector 对象则会因为超出作用域而自动销毁,但我们可以通过编译器的 Named Return Value Optimization 对此进行优化,因此不必担心按值返回会带来性能问题。

虽然我们通过行和列等二维概念定义矩阵,但它的实现是通过 vector 对象模拟的,因此在使用的时候我们需要做一下索引变换,矩阵的第 m 行第 n 列元素对应的 vector 对象的索引是 m * columns + n(m、n 均从 0 开始计算)。假设我们要用 vector 对象模拟一个 3 x 3 的矩阵,如图 1 所示,那么,要访问矩阵的第 2 行第 0 列元素,应该使用索引 6(2 * 3 + 0)访问 vector 对象。

图 1

接下来,我们需要创建三个 array_view 对象,分别包装前面创建的三个 vector 对象,创建的时候先指定行的大小,再指定列的大小,如代码 9 所示。

代码 9

因为我们创建的是二维的 array_view 对象,所以我们可以直接使用二维索引访问矩阵的元素,而不必像前面那样计算对应的索引。还是以 3 x 3 的矩阵为例,如图 2 所示,vector 对象会被分成三段,每段包含三个元素,第一段对应 array_view 对象的第一行,第二段对应第二行,如此类推。如果我们想访问矩阵的第 2 行第 0 列的元素,可以直接使用索引 (2, 0) 访问 array_view 对象,这个索引对应 vector 对象的索引 6。

图 2

考虑到第一、二个 array_view 对象的数据流动方向是从系统内存到显存,我们可以把它们的第一个类型参数改为 const int,如代码 10 所示,表示它们在 Kernel 里是只读的,不会对它包装的 vector 对象产生任何影响。至于第三个 array_view 对象,由于它只是用来输出计算结果,我们可以在调用 parallel_for_each 函数之前调用 array_view 对象的 discard_data 成员函数,表明我们对它包装的 vector 对象的数据不感兴趣,不必把它们从系统内存复制到显存。

代码 10

有了这些准备,我们就可以着手编写 Kernel 了,如代码 11 所示。我们把第三个 array_view 对象的 extent 传给 parallel_for_each 函数,由于这个矩阵是 100 x 100 的,parallel_for_each 函数会创建 10,000 个 GPU 线程,每个 GPU 线程计算这个矩阵的一个元素。由于我们访问的 array_view 对象是二维的,索引的类型也要改为相应的 index<2>。

代码 11

看到这里,你可能会问,GPU 真能创建这么多个线程吗?这取决于具体的 GPU,比如说,NVIDIA GTX 690 有 16 个多处理器( Kepler 架构,每个多处理器有 192 个 CUDA 核),每个多处理器的最大线程数是 2048,因此可以同时容纳最多 32,768 个线程;而 NVIDIA GTX 560 SE 拥有 9 个多处理器( Fermi 架构,每个多处理器有 32 个 CUDA 核),每个多处理器的最大线程数是 1536,因此可以同时容纳最多 13,824 个线程。

计算完毕之后,我们可以在 CPU 上通过索引器访问计算结果,代码 12 向控制台输出结果矩阵的第 14 行 12 列元素。

代码 12

async + continuation

掌握了 C++ AMP 的基本用法之后,我们很自然就想知道 parallel_for_each 函数会否阻塞当前 CPU 线程。parallel_for_each 函数本身是同步的,它负责发起 Kernel 的运行,但不会等到 Kernel 的运行结束才返回。以代码 13 为例,当 parallel_for_each 函数返回时,即使 Kernel 的运行还没结束,checkpoint 1 位置的代码也会照常运行,从这个角度来看,parallel_for_each 函数是异步的。但是,当我们通过 array_view 对象访问计算结果时,如果 Kernel 的运行还没结束,checkpoint 2 位置的代码会卡住,直到 Kernel 的运行结束,array_view 对象把数据从显存复制到系统内存为止。

代码 13

既然 Kernel 的运行是异步的,我们很自然就会希望 C++ AMP 能够提供类似 C++ PPL 的 continuation。幸运的是,array_view 对象提供一个 synchronize_async 成员函数,它返回一个 concurrency::completion_future 对象,我们可以通过这个对象的 then 成员函数实现 continuation,如代码 14 所示。事实上,这个 then 成员函数就是通过 C++ PPL 的 task 对象实现的。

代码 14

你可能会问的问题

  1. 开发 C++ AMP 程序需要什么条件?
    你需要 Visual Studio 2012 以及一块支持 DirectX 11 的显卡,Visual C++ 2012 Express 应该也可以,如果你想做 GPU 调试,你还需要 Windows 8 操作系统。运行 C++ AMP 程序需要 Windows 7/Windows 8 以及一块支持 DirectX 11 的显卡,部署的时候需要把 C++ AMP 的运行时(vcamp110.dll)放在程序可以找到的目录里,或者在目标机器上安装 Visual C++ 2012 Redistributable Package
  2. C++ AMP 是否支持其他语言?
    C++ AMP 只能在 C++ 里使用,其他语言可以通过相关机制间接调用你的 C++ AMP 代码:
  1. C++ AMP 是否支持其他平台?
    目前 C++ AMP 只支持 Windows 平台,不过,微软发布了 C++ AMP 开放标准,支持任何人在任何平台上实现它。如果你希望在其他平台上利用 GPU 做并行计算,你可以考虑其他技术,比如 NVIDIA 的 CUDA (只支持 NVIDIA 的显卡),或者 OpenCL ,它们都支持多个平台。
  2. 能否推荐一些 C++ AMP 的学习资料?
    目前还没有 C++ AMP 的书,Kate Gregory 和 Ade Miller 正在写一本关于 C++ AMP 的书,希望很快能够看到它。下面推荐一些在线学习资料:
2012-08-15 03:0014173

评论

发布
暂无评论
发现更多内容

MediaBox音视频终端SDK已适配鸿蒙星河版(HarmonyOS NEXT)

阿里云CloudImagine

云计算 低代码 sdk

Matlab r2023a 破解版 安装激活教程 含Matlab许可证文件安装密钥

Rose

数学软件 MATLAB R2023a MATLAB安装秘钥

从testcafe迁到playwright你需要注意什么?

QE_LAB

自动化 TestCafe 测试框架 playwright

京东五星门店小程序性能优化实践

京东零售技术

taro 性能优化 前端

从0带你设计与实现基于STM32的智慧农业管理系统

华为云开发者联盟

开发 华为云 stm32 华为云开发者联盟 智慧农业管理系统

【中文无限试用版】intellij idea 2020下载 最好用的Java开发工具 兼容m1

Rose

IntelliJ IDEA激活码 intellij idea 下载 intellij idea 中文 intellij idea 2020破解版

支持M2/M3 macbook高效率工具:Alfred 5汉化包下载

Rose

mac效率工具 Alfred 5破解版 Alfred 中文 Alfred下载

生成式 AI 术语指南:带有配图说明,没有数学公式

Baihai IDP

程序员 AI AIGC 白海科技 GenAI

PostgreSQL技术内幕(十四)探索PG的进程与内存管理

酷克数据HashData

什么是数字化工厂?数字化工厂的整体架构是什么?

万界星空科技

数字化 mes 数字化工厂 万界星空科技

DaVinci Resolve Studio 16 mac达芬奇调色剪辑软件 附注册密钥

Rose

DaVinci Resolve 破解 达芬奇调色软件 达芬奇安装秘钥

Mac 上最好用的 Open 客户端 Viscosity永久激活版 兼容m

Rose

Viscosity mac下载 Open 客户端 Viscosity mac破解

AI会取代低代码吗?——探讨两者在软件开发中的角色和关系

天津汇柏科技有限公司

低代码开发 人工智能、

掌握 Kubernetes 故障排除技巧:kubectl命令的基本指南

SEAL安全

Kubernetes 云原生 kubectl

RESP破解版下载 Redis桌面管理工具 Mac软件下载

Rose

Mac软件 RESP破解版 Redis桌面管理工具

MAMP Pro 6.8.1 Mac永久破解版 Web开发环境 兼容m1

Rose

编程开发 Mac软件 Web开发环境 MAMP PRO激活码 MAMP Pro安装教程

MySQL的JOIN到底是怎么玩的

派大星

:MySQL 数据库 互联网大厂

完美兼容M芯片 Omi NTFS磁盘管理助手下载 NTFS Disk by Omi NTFS Mac

Rose

NTFS Disk by Omi NTFS NTFS 磁盘管理器 ntfs

制造业工厂使用生产管理MES系统前后区别

万界星空科技

数字化转型 制造业 mes 万界星空科技

如何高效接入 Flink: Connecter / Catalog API 核心设计与社区进展

Apache Flink

机器学习 大数据 flink

技术分享 | Selenium多浏览器处理

霍格沃兹测试开发学社

NOT IN子查询中出现NULL值对结果的影响你注意到了吗

GreatSQL

全能解压 mac版 Dr Unarchiver for Mac中文下载

Rose

Mac软件 解压软件 Dr Unarchiver

Mac平台上的强大软件卸载工具:AppDelete中文直装版

Rose

软件卸载工具 Mac卸载软件 苹果电脑软件下载 AppDelete

掌握Python库的Bokeh,就能让你的交互炫目可视化

华为云开发者联盟

Python 开发 数据可视化 华为云 华为云开发者联盟

C++语言现在还有人学吗?

小齐写代码

技术分享 | 网页 frame 与多窗口处理

霍格沃兹测试开发学社

JetBrains DataGrip 2020 编程开发软件 中文无限试用版 兼容m1

Rose

编程 软件 开发

遇见C++ AMP:在GPU上做并行计算_C++_allenlooplee_InfoQ精选文章