C++ AMP异构并行编程解析(一)

2014-11-24 12:06:17 · 作者: · 浏览: 1

微软在今年2月份的GoingNative大会上正式对外发布了C++ AMP(Accelerated Massive Parallelism)开放规范。C++ AMP是微软于11年6月推出的一个异构并行编程框架,从Visual Studio 11开发者预览版起,微软正式提供了C++AMP的支持。C++ AMP的目标是降低在由CPU和GPU共同组成的异构硬件平台上进行数据并行编程(data parallel)的门槛。通过C++ AMP,开发者将获得一个类似C++ STL的库,这个库将作为微软concurrency namespace的一部分,开发者既不需要学习新的C++语法,也不需要更换编译器就能够方便地进行异构并行编程。本文主要介绍C++ AMP的设计原则和语法规则,并将其与CUDA和OpenCL这两个已有的异构并行编程框架进行了对比,希望对大家了解异构并行编程有所帮助。
C++ AMP设计原则
随着CPU由单核向多核转移,多核计算成为了近几年的热点。另一方面,GPU编程也经历着一场变革。传统意义上,GPU一直是作为图形图像专用处理器而存在。然后,因为GPU拥有比CPU还要强大的浮点并行运算能力,我们是不是能让GPU来完成一些通用的计算任务呢?答案是肯定的,例如科学计算中就需要大量的用到浮点计算。在这样的背景下,我们可以将并行计算从单纯的在多核CPU上做,扩展到在多核CPU与GPU共同组成的异构硬件平台上来。除了多核与GPU通用计算的快速发展职位,云计算更成为软件开发的一个重要趋势。实际上,云端的每一台服务器都可以是由多核CPU和GPU共同组成的异构硬件平台。微软的Herb Sutter介绍说:“我们认为多核编程、GPU编程和云计算根本不是三个独立的趋势。实际上,他们只是同一种趋势的不同方面,我们把这个趋势叫做异构并行编程”。进行异构并行编程需要一个统一的编程模型,这就是微软推出C++ AMP的原因。
微软决定另起炉灶,推出C++ AMP这样一个全新的异构并行编程模型的原因很简单,他们认为这个编程模型必须同时具备下面这六个特征,而目前已有的CUDA和OpenCL并不同时满足这些需求。
C++ 而不是 C :这种编程模型应该利用好C++丰富的语言特性(例如抽象,模板,例外处理等),并且不会牺牲性能,因此我们不能像OpenCL一样只是C语言的一种方言;
主流 : 这个编程框架应该能被成千上万的开发者所使用,而不是只被少数人所接受。一个立见分晓的检验办法是:用该编程框架实现GPU上的hello world是只需要几行代码,还是需要几十行才行?
最小的改动 : 这个编程模型应该只需要在C++上进行最小的改动就能够实现应有的功能。通过一个非常小的、具有良好设计的语言扩展,我们就可以把绝大部分复杂的实现交由运行时系统/库去完成。
可移植的。 这种编程模型应该让用户只需要一个二进制可执行文件就可以在任何厂商的GPU硬件上面运行。目前我们使用Direct Compute来实现Windows上所有支持DX11的 GPU上的C++ AMP编程模型,但是未来我们会根据用户的需求在其他异构硬件平台上做相应的实现。
通用且不会过时 。C++ AMP目前针对的是GPU并行计算。但是我们希望,将来C++ AMP的程序可以无缝的扩展到其他形式的计算单元上去,例如FPGA,云端的CPU/GPU处理器等等。
开放 。微软将吧C++ AMP做成一个开放标准,我们鼓励第三方在任何硬件和操作系统上实现C++ AMP编译器和运行时系统。目前AMD和Nvidia都已经声明将会支持C++ AMP。
C++ AMP介绍
下面让我们通过一个简单的程序来了解一下C++ AMP的一些语法规则。首先我们需要引用amp.h这个头文件。C++ AMP中的模板都在concurrency这个命名空间内,所以也需要引用。在C++ AMP中主要有array和array_view这两种数据容器。这两者主要的区别在于array类型的数据在创建时会在GPU显存上拥有一个备份,在GPU对该数据进行完运算之后,开发者必须手动将数据拷贝回CPU。与之相比,array_view其实是一个数据结构的封装,只有在它指向的数据被GPU调用时才会被拷贝到GPU上进行相应的计算。从下例中我们看到,声明array_view数据时需要提供两个模板参数:array_view元素的类型和数据结构的纬度。因为aCPP,bCPP和sumCPP都是一维数组,因此我们在声明时传入int和1两个参数。
接下来就是最重要的计算部分了。parallel_for_each这个方法就是执行在GPU部分的代码的入口。可以看到,parallel_for_each有两个参数,第一个名为sum.extent的参数是用于描述并行计算拓扑结构的对象。通过这个变量,我们指定有多少个GPU线程来并行执行该计算任务,以及这些线程的排列方式。Sum.extend可以理解为按照sum的数据纬度来分配相应数目的GPU线程。Parallel_for_each的第二个参数是一个名为“[=] (index<1> idx) restrict(amp)”的lambda表达式。方括号里的“=”代表了表示lambda表达式的捕获列表。具体来说,“[=]”表示lambda里捕捉的变量按照传值的方式来引用。该for循环的主要参数就是index<1> idx了,它其实代表的是GPU线程的编号。因为之前我们已经通过sum.extent定义好了GPU线程的数量和拓扑结构,因此这个index参数代表的就是一维的数组,即从0到4共5个数。最后一个参数restrict(amp)用来表示parallel_for_each的函数体运行在默认GPU设备上。当然我们也可以定义出amp之外的其他的语法约束,具体的内容请大家参考[1]中的内容。在这之后就是循环体了。这个例子的循环体非常简单,就是让GPU用5个线程并行地把数组a和b中的元素依次相加并存到sum数组中去。
#include
#include
using namespace concurrency;

void CampMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};

// Create C++ AMP objects.
array_view a(5, aCPP);
array_view b(5, bCPP);
array_view sum(5, sumCPP);

parallel_for_each(
// Define the compute domain, which is the set of threads that are created.
sum.extent,
// Define the code to run on each thread on the accelerator.
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);

// Print the results. The expected output is "7, 9, 11, 13, 15".
for (int i = 0