1.3 C++ AMP方法
C++ AMP:用Visual C++加速大规模并行计算
C++ AMP是一个并行库和语言层面的小扩展,能够帮助在C++应用程序中实现异构计算。(AMP是Accelerated Massive Parallelism的缩写,即加速大规模并行。)Visual Studio提供了新的工具和功能支持,可以用来调试和剖析C++ AMP应用程序的性能,包括GPU调试和GPU并行可视化。有了C++ AMP以后,主流C++开发人员可以使用熟悉的工具来创建可移植的、不会过时的应用程序,对于适宜数据并行的应用程序而言,能够实现显著加速。
1.3.1 C++ AMP将GPGPU(以及更多)带进主流
C++ AMP的使命之一是将GPGPU编程带给每一位开发者,使他们的应用程序可以从中获益。现在,支持C++ AMP的显卡几乎无处不在。然而,C++ AMP的首要任务不仅仅是GPGPU,它是一种利用异构计算平台的方法,例如GPU和CPU的向量处理单元,使数以百万计的主流开发人员能够创造性地使用异构平台。尽管说转向数据并行编程,尤其是用C++实现可移植性,是一项巨大的工程,但从软件开发实践的角度看,C++ AMP并非是第一个吃螃蟹的。
许多改变我们的行业和世界的技术或工艺都肇始于研究领域与学术界,这些技术和工艺起初仅被极少数的开发者所掌握,他们使用非常专业的工具,能够做到非常困难的事情。为改变行业和世界,这些技术走向大众,被视为主流。其他技术(例如GUI界面)也发生了这个过程。起初,只有少数开发者拥有诸如控制、响应鼠标事件这样的工作必需的专业技能。随着库、框架和工具的不断开发和发布,越来越多的开发人员能开发GUI应用程序了,现在这些都已成为规范。一些库、框架和工具比其他库、框架和工具更受欢迎,这些都使得GUI开发生态环境得以繁荣昌盛。
类似的过程在面向对象开发上也发生过。起初,当主流开发者还在使用过程化的方式开发应用时,少数学者就已经开始倡导新的软件设计和构建方式了。随着框架和工具的不断开发和发布,采纳规模已经达到了可以视面向对象开发为标准的程度,基本上大多数主流语言的所有开发者都在不同程度地使用它。
这种改变可能正发生在触摸界面和自然用户界面上,并行革命也势在必行。第一阶段是CPU并行,第二阶段是异构并行。要有工具、库和框架才能使异构计算变得更加容易和普遍。C++ AMP和Visual Studio正是主流开发人员需要的可以发挥GPU功能的工具、库和框架。
一个有趣的可能性是,主流开发人员可能会发现不直接使用C++ AMP也能从中获得好处。如果代码库的开发人员采用C++ AMP,使用这些库的代码无需了解底层库的做法就能获得加速。这一点对于创建面向特定领域的代码库而言很重要。
1.3.2 C++ AMP是C++,而不是C
GPGPU的开发还有许多其他方法,这些方法都涉及类C语言。虽然C是一种功能强大的高性能语言,但C++仍然会是那些 喜欢使用现代程序设计语言来工作的注重性能的开发人员的头号选择。C++提供了抽象和类型安全的泛型,开发人员能够借此解决较大的问题,使用更强大的库和结构,我们使用C++ AMP时也可以使用这些特性。我们可以使用模板、重载和异常,就像我们在应用程序其他部分做的那样。
因为C++ AMP是C++,而不是C或类C的语言,所以我们进行并行开发所需要的额外类型,并不是语言本身的扩展或补充,而是模板类型。这为我们提供了类型安全的泛型——我们可以区分实数数组和整数数组,同时降低了学习曲线。增加C语言的抽象和有用类型是C++的主要设计目标之一。
标准C++(例如C++ 11)以前就支持CPU独享编程。C++并行模式库PPL以标准库的方式提供了一组类型和算法来支持C++多核开发,这使得C++开发人员可以运用他们正在使用的语言和工具来利用新硬件。C++ AMP为异构计算带来了同样的舒适和便利。
1.3.3 C++ AMP使用了我们熟识的工具
Visual Studio 2012已全面支持C++ AMP,Windows计算机也将马上可以使用C++ AMP了。这为在Visual Studio中使用C++的所有开发人员打开了方便之门。这些开发人员不需要学习新工具或新语言便能利用强大的GPU。但他们仍要掌握数据并行的思维方式,评估他们所做的算法和数据结构的决策的成本(以执行时间或功耗来计算)。借用熟悉的工具整体技能差距会被拉平。Visual Studio提供了智能感知、GPU调试、性能剖析,以及其他相关功能,这使得开发人员要做的事情远不止编写和编译代码。
即使对于不以Windows为目标平台的开发人员,Visual Studio也是很受欢迎的。更重要的是,C++ AMP开发并非仅限于Windows或Visual Studio用户,它已经发布成为一个开放的技术规范,其他厂商也正在将C++ AMP添加到他们的工具集中。例如,AMD决定把C++ AMP加入到他们的FSA参考编译器中,可以同时支持Windows平台和非Windows平台。
1.3.4 C++ AMP是一个近乎全面的代码库
用熟悉的程序语言编写代码主要是为了保持熟悉的感觉。C++ AMP是对C++的扩展,确实有两个C++11中没有声明的关键字。然而,也只是两个关键字而已,语言的变化并不大。此外,新的主关键字restrict在C99中有声明,因此是一个保留字,不太可能导致与现有代码库的冲突。C++ AMP的其他工作要素还有类型和函数库。对于熟悉标准库或PPL的开发人员,C++ AMP上手也会很快。
下面是一个简单的例子,这是以前在没有考虑并行的条件下,两个向量进行加法运算的传统代码:
void AddArrays(int n, const int const pA, const int const pB, int* const pC)
{
for (int i = 0; i < n; ++i)
{
pC[i] = pA[i] + pB[i];
}
}```
上述代码读起来并不难,而且也容易懂。下面的代码使用GPU,可以使向量加法运算高度并行,代码变化以黑体表示:
include
using namespace concurrency;
void AddArrays(int n, const int const pA, const int const pB, int* const pC)
{
array_view a(n, pA);
array_view b(n, pB);
array_view c(n, pC);
parallel_for_each(c.extent, = restrict(amp)
{
c[idx] = a[idx] + b[idx];
});
}```
如上所示,代码变化真的不大。代码的具体变化如下。
(1)包含amp.h文件以使用C++ AMP库。
(2)因为类型和函数都在concurrency命名空间里,添加using语句以减少键入代码量。
(3)使用数组视图来管理加速器来往数据的复制。
(4)将语言的for变成库函数的parallel_for_each,使用lambda表达式作为函数调用的最后一个参数。
(5)使用restrict(amp)子句识别加速器兼容的代码。
这就是要做的变化。项目配置项和环境变量没有任何变化,其他地方也没有它需要调用的代码。这就是全部的变化。
这些代码的背后发生了什么?简单来说就是,当应用程序被编译的时候,传递给parallel_for_each的lambda表达式被编译成HLSL语言。C++ AMP的运行时库是包含在Visual C++发行组件包里面的一个DLL,它会负责在运行时把HLSL字节码编译成特定硬件的机器码。我们使用C++ AMP的时候,不需要知道这个过程,这是C++ AMP库的任务。
在上述代码中,我们看不到任何将两个输入数组pA和pB复制到加速器的代码,也看不到任何将结果复制回pC的代码。对象array_view负责处理这些工作。array_view是一种可移植视图,不管CPU内存与GPU内存是在同一块芯片上,还是在不同的芯片上,它都可以通过CPU内存与GPU内存的抽象层进行工作。我们可以仿照这个例子封装C格式数组,构造一个array_view,还可以对std :: vector进行封装,如果我们的数据在其中的话。
我们还可以暗示复制要求。比如下述函数的开始部分:
void MatrixMultiply(std::vector<float>& C,
const std::vector<float>& A, const std::vector<float>& B,
int M, int N, int W)
{
array_view<const float, 2> a(M, W, A);
array_view<const float, 2> b(W, N, B);
array_view<float, 2> c(M, N, C);
c.discard_data();```
从前两个array_view对象的声明看,它们是常量浮点数数组。这说明处理完成后不需要将它们从加速器同步回来,它们只是加速器的输入。与其类似,第三个array_view是浮点数,它关联的是C变量,discard_data()调用表明无论内存里面是什么值,对别人都没有意义,所以没有必要将C变量中的初始值复制给加速器。这使得array_view的创建过程非常快。当array_view对象在CPU上被访问或当它们失效时,无论哪一个先发生,结果都会从加速器那里复制回来。
这种暗示方法并不需要新的语言关键字,通过模板重载即可实现。没有开发人员要学习的新编程范例。
原来的数学逻辑(虽不很好)保持不变,完全可读。除了将矩阵元素相加求和以外,这里没有提到多边形、三角形、网格、顶点、纹理、内存或其他任何东西。这就是为什么C++ AMP可以使异构计算成为主流的原因。
下一章的案例研究中,会进一步介绍parallel_for_each的参数细节和新增的restrict关键字的用法。
###1.3.5 C++ AMP可以生成可移植的、不会过时的执行代码
代码被编译后,相同的可执行文件可以在各种计算机上运行,只要这些计算机中有DirectX 11驱动程序就可以,Windows 7及更高版本和Windows Server 2008 R2及更高版本中都有此驱动。不会只限于特定的供应商或显卡系列。
正确的编码可以保证应用程序感知到运行环境,利用好所有可用的加速技术。如果计算机有带DX11驱动程序的硬件,就会实现加速。部署很简单,把可执行文件和一些相关的动态链接库(DLL)(包含在Visual C++可再发行组件包中)复制到目标计算机上即可。
例如,某个可执行程序被复制到不同的计算机上。在无法访问GPU的虚拟机上产生以下输出内容:
CPU exec time: 112.206 (ms)
No accelerator available```
在带有近期经典主流显卡NVIDIA GeForce GT 420的计算机(比拥有虚拟机的笔记本电脑强大)上,产生以下输出内容:
CPU exec time: 27.2373 (ms)
GPU exec time including copy-in/out: 19.8738 (ms)```
巨大的性能提升是通过一个确定哪些加速器可用的简单查询获得的:
`
std::vector<accelerator> accelerators = accelerator::get_all();`
随后我们可以检查返回向量。如果向量为空,就表明没有可用加速器。在尝试执行依赖加速器的代码之前,总要先确保有可用加速器,这是一个最佳实践。形成习惯后,可以让我们的应用程序在各种目标机上工作,同时对最终用户施以最小限制。倘若开发人员安装了Visual Studio,那么他就始终会有一个加速器(可能只是用于调试的仿真器),所以如果在运行时忘记检查是否至少有一个可用加速器的话,就很容易导致经典的“只能在开发机上运行”的场景。
C++ AMP不仅使可执行程序可以在各种计算机上工作,而且还是不会过时的。将来我们编写的利用GPU加速的代码可能会部署在云端,在若干计算机上运行,还可能会以多线程的方式仅在CPU上运行。将来的异构环境将不仅限于CPU+GPU,因此,C++ AMP不只是一个GPU解决方案,还是一个支持将数据并行算法有效映射到多种硬件平台的异构计算解决方案。
随着多核编程逐渐成为主流,我们也会在比较普通的计算机上配4核、8核或16核。通过一些额外工作,我们也能利用其中每个内核的向量单元(使用SSE,AVX或WARP)。GPGPU编程意味着今天我们可以将工作分配给成百上千个硬件线程,甚至在不久的将来还可以分配给更多的硬件线程。云计算中使用的基础设施即服务(Infrastructure as a Service,IaaS)或硬件即服务(Hardware as a Service,HaaS)产品,可以想象成是成千上万的硬件线程。试想如果我们能将两者结合起来,在云计算机上也使用GPU核,达到数以千万计的硬件线程规模,将会发生什么?