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 建立,並共用 的 access_typearrayaccelerator_view

下列程式碼範例示範如何判斷預設加速器是否支援共用記憶體,然後建立數個具有不同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 變數,在磚 上累積值。 然後,您可以完成任何需要存取所有值的計算。

下圖代表以磚排列的二維取樣資料陣列。

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::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 、、 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)
原生程式碼部落格中的平行程式設計