共用方式為


C++ AMP 概觀

注意

從 Visual Studio 2022 17.0 版開始,C++ AMP 標頭已被取代。 包含任何 AMP 標頭將會產生建置錯誤。 先定義 _SILENCE_AMP_DEPRECATION_WARNINGS ,再包含任何 AMP 標頭以讓警告無聲。

C++加速大規模平行處理原則(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_view 介面,本文稍後會討論。

  • 反覆 專案:parallel_for_each函式 (C++ AMP) 提供逐一查看數據元素或 計算網域的機制。 在此範例中,計算網域是由 指定 sum.extent。 您想要執行的程式代碼包含在 Lambda 運算式或 核心函式中。 restrict(amp)表示只會使用C++ AMP 可以加速C++語言的子集。

  • Index: index Class 變數會 idx以一個等級宣告,以符合物件的排名 array_view 。 藉由使用索引,您可以存取 物件的個別專案 array_view

成形和編製索引數據:索引和範圍

您必須先定義數據值,並宣告數據的形狀,才能執行核心程序代碼。 所有數據都定義為陣列(矩形),而且您可以定義數位以具有任何排名(維度數目)。 數據可以是任何維度中的任何大小。

index 類別

索引類別會藉由將每個維度中原點的位移封裝成一個物件,以指定 或 array_view 物件中array的位置。 當您存取陣列中的位置時,您會將 對象傳遞 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

下列範例會建立一個三維索引,指定深度 = 0、數據列 = 1,以及三維 array_view 對象中的數據行 = 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 類別

範圍類別會指定 或 array_view 物件之每個維度array中的數據長度。 您可以建立範圍,並用它來建立 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 ,其維度與上一個範例中的物件相同,但此範例會使用 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_view

運行時間連結庫中定義了用來將數據移至加速器的兩個數據容器。 它們是陣列類別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 建構函式的數據不會在 GPU 上複寫,因為它與建構函式一 array 樣。 相反地,當核心函式執行時,數據會複製到加速器。 因此,如果您建立兩個 array_view 使用相同的數據的物件,這兩個 array_view 對象都會參考相同的記憶體空間。 當您這樣做時,您必須同步處理任何多線程存取。 使用 array_view 類別的主要優點是數據只有在必要時才移動。

陣列和array_view的比較

下表摘要說明 和 array_view 類別之間的array相似性和差異。

描述 array 類別 array_view 類別
判斷排名時 在編譯時期。 在編譯時期。
判斷範圍時 在運行時間。 在運行時間。
圖形 矩形的。 矩形的。
資料存放區 這是數據容器。 這是數據包裝函式。
複本 定義時的明確和深層複製。 核心函式存取隱含複製時。
資料擷取 將陣列資料複製到 CPU 線程上的物件。 藉由直接存取 array_view 物件或呼叫 array_view::synchronize 方法 ,以繼續存取原始容器上的數據。

具有陣列和array_view的共享記憶體

共用記憶體是 CPU 和加速器可以存取的記憶體。 使用共用記憶體可消除或大幅減少在 CPU 與加速器之間複製數據的額外負荷。 雖然記憶體已共用,但 CPU 和加速器無法同時存取記憶體,因此會導致未定義的行為。

array 如果相關聯的加速器支持共用記憶體,物件可用來指定使用共用記憶體的精細控制。 加速器是否支持共用記憶體是由加速器的 supports_cpu_shared_memory 屬性所決定,當支援共用記憶體時,就會傳回 true 該屬性。 如果支援共用記憶體,則加速器上記憶體配置的預設 access_type列舉 是由 default_cpu_access_type 屬性所決定。 根據預設,arrayarray_view 物件會接受與主要相關聯的 accelerator相同 access_type

藉由明確設定 的 array::cpu_access_type Data Member 屬性 array ,您可以根據其計算核心的記憶體存取模式,對共用記憶體的使用方式執行精細的控制,以便根據硬體的效能特性優化應用程式。 array_viewcpu_access_type反映與其array相關聯的 ,或者,如果建構array_view沒有數據源,則其access_type會反映第一次導致它配置記憶體的環境。 也就是說,如果第一次由主機存取 (CPU),則其行為就像是透過 CPU 數據源建立,並透過擷取共用access_type相關聯的 ;不過,如果第一次由 accelerator_view存取,則其行為就像是透過該建立的 建立而accelerator_viewarray建立,並共用 的 。accelerator_viewaccess_typearray

下列程式代碼範例示範如何判斷預設加速器是否支援共用記憶體,然後建立數個具有不同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 運算式會定義在每個線程上執行的程式代碼。 擷取子句 [=]會指定 Lambda 運算式主體依值存取所有擷取的變數,在此案例中為 absum。 在此範例中,參數清單會建立名為idx的一維index變數。 idx[0]的值在第一個線程中為0,並在每個後續線程中增加一個。 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 Keyword 關鍵詞和 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::p recise_math 命名空間中的雙精確度連結庫支援雙精確度函式。 它也提供單精度函式的支持,不過仍需要硬體上的雙精確度支援。 它符合 C99 規格(ISO/IEC 9899)。 快速鍵必須支援完整雙精確度。 您可以藉由檢查 accelerator::supports_double_precision Data Member 的值來判斷它是否執行。 Concurrency::fast_math Namespace 中的快速數學連結庫包含另一組數學函式。 這些函式僅 float 支援操作數,執行得更快,但不像雙精確度數學連結庫中的函式那麼精確。 函式包含在 <amp_math.h> 頭檔中,且所有函式都會使用 restrict(amp)宣告。 cmath> 頭檔中的<函式會同時匯入 fast_mathprecise_math 命名空間。 關鍵詞 restrict 可用來區分 <cmath> 版本和 C++ AMP 版本。 下列程式代碼會使用fast方法計算計算定義域中每個值的base-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 Namespace 中,並包含在 <amp_graphics.h> 頭檔中。 圖像庫的主要元件包括:

  • texture 類別:您可以使用紋理類別,從記憶體或檔案建立紋理。 紋理類似數位,因為它們包含數據,而且它們與指派和複製建構相關的C++標準連結庫中的容器類似。 如需詳細資訊,請參閱 C++ 標準程式庫容器。 類別的 texture 範本參數是元素類型和排名。 排名可以是 1、2 或 3。 元素類型可以是本文稍後所述的其中一個簡短向量型別。

  • writeonly_texture_view類別:提供任何紋理的僅限寫入存取權。

  • 短向量連結庫:定義一組長度為 2、3 和 4 的短向量類型,其以 intuintfloat、、 doublenormunorm 為基礎。

通用 Windows 平台 (UWP) 應用程式

如同其他C++連結庫,您可以在UWP app中使用 C++ AMP。 這些文章說明如何在使用 C++、C#、Visual Basic 或 JavaScript 建立的應用程式中包含 C++ AMP 程式代碼:

C++ AMP 和並行可視化檢視

並行可視化檢視包含分析 C++ AMP 程式代碼效能的支援。 這些文章描述這些功能:

效能建議

無符號整數的模數和除法效能明顯優於帶正負號整數的模數和除法。 建議您盡可能使用不帶正負號的整數。

另請參閱

C++ AMP (C++ Accelerated Massive Parallelism)
Lambda 運算式語法
參考 (C++ AMP)
原生程式代碼部落格中的平行程序設計