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 类通过将每个维度中与原点的偏移量封装到一个对象中,来指定 arrayarray_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 类指定 arrayarray_view 对象的每个维度中的数据长度。 可以创建一个范围并使用它来创建 arrayarray_view 对象。 还可以检索现有 arrayarray_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 的比较

下表总结了 arrayarray_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 属性确定。 默认情况下,arrayarray_view 对象采用与主要关联 accelerator 相同的 access_type

通过显式设置 arrayarray::cpu_access_type 数据成员属性,可以对共享内存的使用方式进行精细控制,这样就可以根据硬件计算内核的内存访问模式,针对硬件的性能特征优化应用。 array_view 反映与其关联的 array 相同的 cpu_access_type;如果 array_view 是在没有数据源的情况下构造的,其 access_type 将反映第一个导致其分配存储的环境。 也就是说,如果它首先由主机 (CPU) 访问,那么它的行为就像它是在 CPU 数据源上创建的一样,并共享由捕获关联的 accelerator_viewaccess_type;但是,如果它首先由 accelerator_view 访问,那么它的行为就像它是在该 accelerator_view 上创建的 array 上创建的一样,并共享 arrayaccess_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 函数定义要在加速器上针对 arrayarray_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 表达式的主体按值访问所有捕获的变量,在本例中为 absum。 在此示例中,参数列表创建一个名为 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";
    }
}

加速代码: 平铺和屏障

可以使用平铺获得额外的加速。 平铺将线程分成相等的矩形子集或图块。 可以根据数据集和正在编码的算法确定适当的图块大小。 对于每个线程,可以访问数据元素相对于整个 arrayarray_view 的全局位置,以及访问相对于图块的本地位置。 使用本地索引值可以简化代码,因为无需编写代码将索引值从全局转换为本地。 若要使用平铺,请在 parallel_for_each 方法中对计算域调用 extent::tile 方法,并在 Lambda 表达式中使用 tiled_index 对象。

在典型应用程序中,图块中的元素以某种方式相关,代码必须访问并跟踪图块中的值。 可使用 tile_static 关键字tile_barrier::wait 方法完成此操作。 包含 tile_static 关键字的变量具有跨整个图块的作用域,并且将为每个图块创建该变量的实例。 必须处理对该变量的图块线程访问的同步。 tile_barrier::wait 方法可停止当前线程的执行,直到图块中的所有线程都到达对 tile_barrier::wait 的调用。 因此,可以使用 tile_static 变量累积图块中的值。 然后,可以完成任何需要访问所有值的计算。

下图表示以图块排列的采样数据的二维数组。

Index values in a tiled extent.

以下代码示例使用上图中的采样数据。 代码将图块中的每个值替换为图块中值的平均值。

// 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_mathprecise_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 的短向量类型,它们基于 intuintfloatdoublenormunorm

通用 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)
本机代码中的并行编程博客