C++ AMP 概述
C++ Accelerated Massive Parallelism (C++ AMP) 可加速 C++ 代码执行,方法是通过利用数据并行硬件(例如离散图像卡上的图像处理单元 (GPU))。 通过使用 C++ AMP,您可以为多维数据算法编码,以便通过使用异类硬件上的并行对执行进行加速。 C++ AMP 编程模型包括多维数组、索引,内存传输、平铺和数学函数库。 您可以使用 C++ AMP 语言扩展控制数据在 CPU 和 GPU 之间相互移动的方式,从而提高性能。
-
Windows 7、Windows 8、Windows Server 2008 R2 或 Windows Server 2012
-
DirectX 11 功能级别 11.0 或更高的硬件
-
对于在软件模拟器上进行的调试,需要 Windows 8 或 Windows Server 2012。 对于在硬件上的调试,必须为图形卡安装驱动程序。 有关详细信息,请参阅 调试 GPU 代码。
以下两个实例阐释 C++ AMP 的主要组成部分。 假定您要添加两个一维数组的相应元素。 例如,您可能想要添加 {1, 2, 3, 4, 5} 和 {6, 7, 8, 9, 10}以获取 {7, 9, 11, 13, 15}。 如果不使用 C++ AMP,您可编写以下代码以添加数字并显示结果。
#include <iostream> void StandardMethod() { int aCPP[] = {1, 2, 3, 4, 5}; int bCPP[] = {6, 7, 8, 9, 10}; int sumCPP[5]; for (int idx = 0; idx < 5; idx++) { sumCPP[idx] = aCPP[idx] + bCPP[idx]; } for (int idx = 0; idx < 5; idx++) { std::cout << sumCPP[idx] << "\n"; } }
代码的重要部分如下所示:
-
数据:该数据包括三个数组。 所有数组都具有相同的秩 (1) 和长度 (5)。
-
迭代:第一个 for 循环通过数组中的元素提供迭代机制。 您希望执行(以计算总和)的代码包含在第一个 for 块中。
-
索引:idx 变量访问数组的各个元素。
使用 C++ AMP,您可以编写以下代码。
#include <amp.h> #include <iostream> using namespace concurrency; const int size = 5; void CppAmpMethod() { int aCPP[] = {1, 2, 3, 4, 5}; int bCPP[] = {6, 7, 8, 9, 10}; int sumCPP[size]; // Create C++ AMP objects. array_view<const int, 1> a(size, aCPP); array_view<const int, 1> b(size, bCPP); array_view<int, 1> sum(size, sumCPP); sum.discard_data(); 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; i < size; i++) { std::cout << sum[i] << "\n"; } }
尽管存在相同的基本元素,但是使用 C++ AMP 构造:
-
数据:您使用 C++ 数组构造三个 C++ AMP array_view 对象。 您提供四个值以构造array_view对象:数据值、秩、元素类型和每个维度中的对象长度array_view。 等级和类型作为类型参数进行传递。 数据和长度作为构造函数参数传递。 在此示例中,传递给构造函数的 C++ 数组是一维的。 等级和长度用于在 array_view 对象中构造矩形的数据,并且数据值用于填充数组。 运行库还包括 array 类,其具有一个类似于 array_view 选件类的接口,稍后对此进行讨论。
-
迭代:parallel_for_each 函数 (C++ AMP) 通过数据元素或计算域提供迭代机制。 在此示例中,计算域由 sum.extent 指定。 您希望执行的代码包含在 lambda 表达式或内核函数中。 restrict(amp) 表明只能使用 C++ AMP 可以加快的 C++ 语言子集。
-
索引:index 类 变量、idx,声明了一个级别以匹配 array_view 对象的级别。 通过使用索引,您可以访问 array_view 对象的单个元素。
您必须在运行内核代码前,定义数据值并声称数据形状。 所有数据都被定义为数组(矩形),并且您可以任意定义数组的秩(维数)。 数据可以是任何维度任何大小。
index 类
index 类 通过将每个维度原点的偏移量封装到一个对象,进而指定 array 或 array_view 对象中的位置。 在访问数组中的某个位置时,会将 index对象传递给索引运算符,[],而不是整数索引列表。 可通过使用 array::operator() 运算符 或 array_view::operator() 运算符 访问每个维度中的元素。
以下示例创建了一个一维索引,该索引指定了一维 array_view 对象中的第三个元素。 索引用于打印 array_view 对象的第三个元素。 输出为 3。
int aCPP[] = {1, 2, 3, 4, 5}; array_view<int, 1> a(5, aCPP); index<1> idx(2); std::cout << a[idx] << "\n"; // Output: 3
以下示例创建一个二维索引,该索引为二维 array_view 对象指定元素,其中行数为 1,列数为 2。 index 构造函数中的第一个参数是行组件,第二个参数是列组件。 输出为 6。
int aCPP[] = {1, 2, 3, 4, 5, 6}; array_view<int, 2> a(2, 3, aCPP); index<2> idx(1, 2); std::cout << a[idx] << "\n"; // Output: 6
以下示例创建一个三维索引,该索引为三维 array_view 对象指定元素,其中深度为 0,行数为 1,列数为 3。 注意,第一个参数是深度组件,第二个参数是行组件,第三个参数是列组件。 输出为 8。
int aCPP[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; array_view<int, 3> a(2, 3, 4, aCPP); // Specifies the element at 3, 1, 0. index<3> idx(0, 1, 3); std::cout << a[idx] << "\n"; // Output: 8
extent 类
extent 类 (C++ AMP) 指定 array 或 array_view 对象的每一维度的数据长度。 您可创建范围并使用它来创建 array 或 array_view 对象。 也可检索现有的 array 或 array_view 对象的范围。 以下示例打印了 array_view 对象的每个维度内容的长度。
int aCPP[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; // There are 3 rows and 4 columns, and the depth is two. array_view<int, 3> a(2, 3, 4, aCPP); std::cout << "The number of columns is " << a.extent[2] << "\n"; std::cout << "The number of rows is " << a.extent[1] << "\n"; std::cout << "The depth is " << a.extent[0]<< "\n"; std::cout << "Length in most significant dimension is " << a.extent[0] << "\n";
以下示例创建的 array_view 对象与先前示例创建的对象有相同的维度,但是该示例使用 extent 对象而不是使用 array_view 构造函数的显式参数。
int aCPP[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}; extent<3> e(2, 3, 4); array_view<int, 3> a(e, aCPP); std::cout << "The number of columns is " << a.extent[2] << "\n"; std::cout << "The number of rows is " << a.extent[1] << "\n"; std::cout << "The depth is " << a.extent[0] << "\n";
用于将数据移动到加速器的两个数据容器是在运行时库中进行定义。 它们是 array 类 和 array_view 类。 array 类是在构造对象时创建了数据深层复制的容器类。 array_view 类是在内核函数访问数据时复制数据的包装器类。 当源设备上需要数据时,则复制回数据。
array 类
当构造 array 对象时,如果您使用将指针包括在数据集中的构造函数,则会在加速器上创建数据的深层复制。 核函数修改快捷键对应的副本。 内核函数的执行完成后,您必须将数据复制回源数据结构。 以下示例将向量的每个元素乘以 10。 内核函数完成后,向量转换运算符会用于复制数据并传回向量对象。
std::vector<int> data(5); for (int count = 0; count < 5; count++) { data[count] = count; } array<int, 1> a(5, data.begin(), data.end()); parallel_for_each( a.extent, [=, &a](index<1> idx) restrict(amp) { a[idx] = a[idx] * 10; } ); data = a; for (int i = 0; i < 5; i++) { std::cout << data[i] << "\n"; }
array_view 类
array_view 与 array 类的成员几乎相同,但基础行为不同。 传递给 array_view 构造函数的数据不会再 GPU 上进行复制,因为它与 array 构造函数在一起。 相反,当核函数执行时,将数据复制到快捷键。 因此,如果创建使用相同数据的两个 array_view 对象,则两个 array_view 对象均会引用同一内存空间。 如果这样做,则必须同步所有多线程的访问。 使用 array_view 类的主要优点是数据仅在必要的时候移动。
array 和 array_view 的比较
下表总结了 array 和 array_view 类之间的相同点和不同点。
描述 | 数组类 | array_view 类 |
---|---|---|
什么时候确定级别 | 在编译时。 | 在编译时。 |
什么时候确定范围 | 在运行时。 | 在运行时。 |
形状 | 矩形。 | 矩形。 |
数据存储 | 是数据容器。 | 是数据包装器。 |
复制 | 对定义的显式和深层复制。 | 当内核函数对其进行访问时,将进行隐式复制。 |
数据检索 | 通过将数组数据复制回 CPU 线程上的对象中。 | 通过直接访问 array_view 对象或调用 array_view::synchronize 方法 来继续访问原始容器上的数据。 |
与数组和 array_view 共享的内存
共享内存是可以同时由 CPU 和快捷键访问的内存。 使用共享内存消除或显著降低 CPU 和快捷键之间复制数据的开销。 虽然内存共享,但不能使用 CPU 和 加速器同时访问内存,如果同时访问则会导致未定义的行为。
如果关联的加速器支持,array 对象可用于指定细化的控件控制对共享内存的使用。 快捷键是否支持共享内存取决于supports_cpu_shared_memory 属性,当共享内存受支持时,此属性会返回 true。 如果支持共享内存,加速器上内存分配的默认default_cpu_access_type 将由 access_type 枚举 属性决定。 默认情况下,array 和 array_view 对象采用和主要关联的 accelerator 相同的access_type。
通过显式设置 array 的 array::cpu_access_type 数据成员 属性,您可以对如何使用共享内存进行细化的控制,这样你就可以根据硬件计算内核的内存访问模式为硬件的性能特征优化应用程序。 array_view 反映和与之相关联的 array 相同的 cpu_access_type;但如果建立 array_view 时没有使用数据源,则它的 access_type 反映首次导致其分配存储的环境。 也就是说,如果它由主机 (CPU) 首先访问,那它就会表现得如同是通过 CPU 数据源创建并共享通过捕获关联起来的 accelerator_view 的 access_type;但是,如果它由 accelerator_view 首次访问,那它就会表现得如同是通过在 accelerator_view 上创建的 array 创建并共享 array 的 access_type。
以下代码示例演示如何确定默认快捷键是否支持共享内存,然后创建具有不同 cpu_access_type 配置的多个数组。
#include <amp.h> #include <iostream> using namespace Concurrency; int main() { accelerator acc = accelerator(accelerator::default_accelerator); // Early out if the default accelerator doesn’t support shared memory. if (!acc.supports_cpu_shared_memory) { std::cout << "The default accelerator does not support shared memory" << std::endl; return 1; } // Override the default CPU access type. acc.default_cpu_access_type = access_type_read_write // Create an accelerator_view from the default accelerator. The // accelerator_view inherits its default_cpu_access_type from acc. accelerator_view acc_v = acc.default_view; // Create an extent object to size the arrays. extent<1> ex(10); // Input array that can be written on the CPU. array<int, 1> arr_w(ex, acc_v, access_type_write); // Output array that can be read on the CPU. array<int, 1> arr_r(ex, acc_v, access_type_read); // Read-write array that can be both written to and read from on the CPU. array<int, 1> arr_rw(ex, acc_v, access_type_read_write); }
parallel_for_each 函数定义了要在快捷键上运行、针对 array 或 array_view 对象中的数据的代码。 考虑以下来自此主题的介绍中的代码。
#include <amp.h> #include <iostream> using namespace concurrency; void AddArrays() { int aCPP[] = {1, 2, 3, 4, 5}; int bCPP[] = {6, 7, 8, 9, 10}; int sumCPP[5] = {0, 0, 0, 0, 0}; array_view<int, 1> a(5, aCPP); array_view<int, 1> b(5, bCPP); array_view<int, 1> sum(5, sumCPP); parallel_for_each( sum.extent, [=](index<1> idx) restrict(amp) { sum[idx] = a[idx] + b[idx]; } ); for (int i = 0; i < 5; i++) { std::cout << sum[i] << "\n"; } }
parallel_for_each 方法采用两个参数、一个计算域和一个 Lambda 表达式。
计算域是 extent 对象或定义要为并行执行创建的线程组的 tiled_extent 对象。 计算域中的每个元素生成一个线程。 在这种情况下,extent 对象是一维的并有五个元素。 因此,启动了五个线程。
“lambda 表达式”定义每个线程上的运行代码。 捕获子句 [=] 指定 lambda 表达式的主体通过值访问所有捕获的变量,在本例中即是 a、b 和sum。 在此示例中,参数列表创建名为 idx 的一维 index 变量。 第一个线程中 idx[0] 的值为 0,并在每个后续线程中加 1。 restrict(amp) 表明只能使用 C++ AMP 可以加快的 C++ 语言子集。有限制修饰符的函数的限制描述在 限制子句 (C++ AMP) 中。 有关更多信息,请参见 Lambda 表达式语法。
Lambda 表达式可以包含执行代码,也可以调用单独的核函数。 核函数必须包含 restrict(amp) 修饰符。 以下示例与前面的示例等效,但是它调用单独的内核函数。
#include <amp.h> #include <iostream> using namespace concurrency; void AddElements(index<1> idx, array_view<int, 1> sum, array_view<int, 1> a, array_view<int, 1> b) restrict(amp) { sum[idx] = a[idx] + b[idx]; } void AddArraysWithFunction() { int aCPP[] = {1, 2, 3, 4, 5}; int bCPP[] = {6, 7, 8, 9, 10}; int sumCPP[5] = {0, 0, 0, 0, 0}; array_view<int, 1> a(5, aCPP); array_view<int, 1> b(5, bCPP); array_view<int, 1> sum(5, sumCPP); parallel_for_each( sum.extent, [=](index<1> idx) restrict(amp) { AddElements(idx, sum, a, b); } ); for (int i = 0; i < 5; i++) { std::cout << sum[i] << "\n"; } }
您可通过使用平铺来获取额外的加速。 平铺将线程划分为相等的矩形子集或图块。 您基于数据集和您编码的算法来确定合适的平铺尺寸。 对于每个线程,可以访问相对于整个 array 或 array_view 的数据元素的全局位置,并访问于平铺的本地位置。 使用本地索引值可简化您的代码,因为您不必编写代码将索引值从全局转换到本地。 若要使用平铺,在 parallel_for_each 方法中的计算域上调用 extent::tile 方法,并在 lambda 表达式中使用 tiled_index 对象。
在典型的应用程序中,在平铺的元素以某种方式相关,因此,代码必须访问和记录在平铺之间的值。 使用 tile_static 关键字 关键字和tile_barrier::wait 方法 完成此操作。 具有 tile_static 关键字的变量的范围是整个平铺,并且该变量为每个平铺创建一个实例。 您必须处理变量的平铺线程访问的同步。 tile_barrier::wait 方法 将停止执行当前线程,直到平铺中的所有线程都达到 tile_barrier::wait 的调用。 您可以使用tile_static 变量累积平铺间的值。 然后,您可以完成任何需要访问所有值的计算。
下图表示排列在平铺内的采样数据的二维数组。
以下代码示例使用上图中的采样数据。 代码以平铺中值的平均值替换平铺中的每个值。
// Sample data: int sampledata[] = { 2, 2, 9, 7, 1, 4, 4, 4, 8, 8, 3, 4, 1, 5, 1, 2, 5, 2, 6, 8, 3, 2, 7, 2}; // The tiles: // 2 2 9 7 1 4 // 4 4 8 8 3 4 // // 1 5 1 2 5 2 // 6 8 3 2 7 2 // Averages: int averagedata[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; array_view<int, 2> sample(4, 6, sampledata); array_view<int, 2> average(4, 6, averagedata); parallel_for_each( // Create threads for sample.extent and divide the extent into 2 x 2 tiles. sample.extent.tile<2,2>(), [=](tiled_index<2,2> idx) restrict(amp) { // Create a 2 x 2 array to hold the values in this tile. tile_static int nums[2][2]; // Copy the values for the tile into the 2 x 2 array. nums[idx.local[1]][idx.local[0]] = sample[idx.global]; // When all the threads have executed and the 2 x 2 array is complete, find the average. idx.barrier.wait(); int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1]; // Copy the average into the array_view. average[idx.global] = sum / 4; } ); for (int i = 0; i < 4; i++) { for (int j = 0; j < 6; j++) { std::cout << average(i,j) << " "; } std::cout << "\n"; } // Output: // 3 3 8 8 3 3 // 3 3 8 8 3 3 // 5 5 2 2 4 4 // 5 5 2 2 4 4
C++ AMP 包括两个数学库。 Concurrency::precise_math 命名空间 中的双精度库支持双精度函数。 它还支持单精度功能,尽管硬件仍需要支持双精度。 其符合 C99 规范 (ISO/IEC 9899)。 快捷键必须支持完全双精度。 您可通过检查 accelerator::supports_double_precision 数据成员 的值来确定是否。 Concurrency::fast_math 命名空间 中的快速数学库包含另一组数学函数。 只支持 float 操作数的这些函数,执行速度更快,但精确度不如双精度数学库中的函数。 函数包含在 <amp_math.h> 头文件中,并且全部声明 restrict(amp)。 <cmath> 头文件中的函数导入到fast_math 和 precise_math 命名空间中。 restrict 关键字用于区分 <cmath> 版本和 C++ AMP 版本。 以下代码使用快速方法计算计算域中每个值的以 10 为底的对数。
#include <amp.h> #include <amp_math.h> #include <iostream> using namespace concurrency; void MathExample() { double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 }; array_view<double, 1> logs(6, numbers); parallel_for_each( logs.extent, [=] (index<1> idx) restrict(amp) { logs[idx] = concurrency::fast_math::log10(logs[idx]); } ); for (int i = 0; i < 6; i++) { std::cout << logs[i] << "\n"; } }
C++ AMP 包括为加速的图形编程而设计的图形库。 此库仅用于支持本地图形功能的设备。 方法在 Concurrency::graphics 命名空间 中,并且包含在 <amp_graphics.h> 头文件中。 图形库的关键组件有:
并发可视化工具包含分析 C++ AMP 代码性能的支持。 这些文章介绍了这些功能:
-
Analyzing C++ AMP Code with the Concurrency Visualizer(使用并发可视化工具分析 C++ AMP 代码)