C++ AMP 概述
注意
从 Visual Studio 2022 版本 17.0 开始,已弃用 C++ AMP 标头。
包含任何 AMP 标头都会导致生成错误。 应在包含任何 AMP 标头之前定义 _SILENCE_AMP_DEPRECATION_WARNINGS
,以使警告静音。
C++ Accelerated Massive Parallelism (C++ AMP) 利用数据并行硬件(例如独立显卡上的图形处理单元 (GPU))来加速 C++ 代码的执行。 通过使用 C++ AMP,可以编写多维数据算法,以便通过在异类硬件上使用并行来加速执行。 C++ AMP 编程模型包括多维数组、索引、内存传输、平铺和数学函数库。 可以使用 C++ AMP 语言扩展来控制如何在 CPU 与 GPU 之间来回移动数据,从而提高性能。
系统要求
Windows 7 或更高版本
Windows Server 2008 R2 至 Visual Studio 2019。
DirectX 11 功能级别 11.0 或更高版本的硬件
若要在软件仿真器上进行调试,需要使用 Windows 8 或 Windows Server 2012。 对于在硬件上进行的调试,您必须为图形卡安装驱动程序。 有关详细信息,请参阅调试 GPU 代码。
注意:ARM64 目前不支持 AMP。
介绍
以下两个示例阐释了 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";
}
}
代码的重要部分如下所示:
数据:数据由三个数组组成。 它们具有相同的轶(一)和长度(五)。
迭代:第一个
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 和 extent
必须先定义数据值并声明数据的形状,然后才能运行内核代码。 所有数据都定义为一个数组(矩形),你可以将数组定义为具有任意秩(维数)。 数据可以为任意维度中的任意大小。
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 类指定 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
对象,该对象与上一个示例中的对象具有相同的维度,但此示例在 array_view
构造函数中使用 extent
对象,而不是使用显式参数。
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_view
类是一个包装类,可在内核函数访问数据时复制数据。 当源设备上需要数据时,数据将复制回来。
array 类
构造 array
对象时,如果使用的构造函数包含指向数据集的指针,则会在加速器上创建数据的深层副本。 内核函数会修改加速器上的副本。 内核函数执行完毕后,必须将数据复制回源数据结构。 以下示例将向量中的每个元素乘以 10。 内核函数完成后,使用 vector conversion operator
将数据复制回向量对象。
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
构造函数的数据不会像传递给 array
构造函数那样在 GPU 上复制, 而是在执行内核函数时将数据复制到加速器。 因此,如果创建两个使用相同数据的 array_view
对象,这两个 array_view
对象将引用相同的内存空间。 执行此操作时,必须同步所有多线程访问。 使用 array_view
类的主要优点在于,仅在必要时移动数据。
array 和 array_view 的比较
下表总结了 array
和 array_view
类之间的异同。
说明 | array 类 | array_view 类 |
---|---|---|
何时确定秩 | 在编译时。 | 在编译时。 |
何时确定范围 | 在运行时。 | 在运行时。 |
形状 | 矩形。 | 矩形。 |
数据存储 | 是数据容器。 | 是数据包装器。 |
复制 | 定义时进行显式和深层复制。 | 内核函数访问时进行隐式复制。 |
数据检索 | 通过将数组数据复制回 CPU 线程上的对象。 | 通过直接访问 array_view 对象或调用 array_view::synchronize 方法,继续访问原始容器上的数据。 |
array 和 array_view 的共享内存
共享内存是 CPU 和加速器都可以访问的内存。 使用共享内存可以消除或显著减少在 CPU 和加速器之间复制数据的开销。 尽管内存是共享的,但 CPU 和加速器不能同时访问它,因为这样做会导致未定义的行为。
array
对象可用于指定对共享内存使用(如果关联的加速器支持)的精细控制。 加速器是否支持共享内存取决于加速器的 supports_cpu_shared_memory 属性,该属性在支持共享内存时返回 true
。 如果支持共享内存,则加速器上内存分配的默认 access_type 枚举由 default_cpu_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
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 表达式定义要在每个线程上运行的代码。 capture 子句 [=]
指定 Lambda 表达式的主体按值访问所有捕获的变量,在本例中为 a
、b
和 sum
。 在此示例中,参数列表创建一个名为 idx
的一维 index
变量。 idx[0]
的值在第一个线程中为 0,在每个后续线程中加 1。 restrict(amp)
指示仅使用 C++ AMP 可以加速的 C++ 语言子集。 restrict (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(numbers[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
图形库
C++ AMP 包含一个专为加速图形编程设计的图形库。 此库仅用于支持本机图形功能的设备。 这些方法位于 Concurrency::graphics 命名空间中,并包含在 <amp_graphics.h> 头文件中。 图形库的关键组件包括:
texture 类:可以使用 texture 类通过内存或文件创建纹理。 纹理类似于数组,因为它们包含数据,并且就赋值和复制构造而言,它们类似于 C++ 标准库中的容器。 有关详细信息,请参阅 C++ 标准库容器。
texture
类的模板参数是元素类型和秩。 秩可以是 1、2 或 3。 元素类型可以是本文后面介绍的短向量类型之一。writeonly_texture_view 类:提供对任何纹理的只写访问。
短向量库:定义一组长度为 2、3 和 4 的短向量类型,它们基于
int
、uint
、float
、double
、norm 或 unorm。
通用 Windows 平台 (UWP) 应用
与其他 C++ 库一样,可以在 UWP 应用中使用 C++ AMP。 以下文章介绍了如何在使用 C++、C#、Visual Basic 或 JavaScript 创建的应用中包含 C++ AMP 代码:
C++ AMP 和并发可视化工具
并发可视化工具支持分析 C++ AMP 代码的性能。 以下文章介绍了这些功能:
性能建议
无符号整数的模数和除法的性能明显优于带符号整数的模数和除法。 建议尽可能使用无符号整数。
另请参阅
C++ AMP (C++ Accelerated Massive Parallelism)
Lambda 表达式语法
参考 (C++ AMP)
本机代码中的并行编程博客