共用方式為


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 資料的矩形形狀,資料值則用來填補陣列。 執行時函式庫還包含陣 列 Class,其介面類似於該 array_view 類別,本文稍後會討論。

  • 迭代: parallel_for_each函數(C++ AMP) 提供一種遍歷資料元素或 計算域的機制。 在此範例中,計算域由 指定為 sum.extent。 你想執行的程式碼包含在 lambda 運算式或 核心函式中。 此 restrict(amp) 表示僅使用 C++ AMP 能加速的 C++ 語言子集。

  • 索引:索引 類別變數idx,以 1 的秩宣告,以匹配物件的 array_view 秩。 透過索引,你可以存取物件的 array_view 各個元素。

塑形與索引資料:指數與範圍

你必須先定義資料值並宣告資料的形狀,才能執行核心程式碼。 所有資料都定義為陣列(矩形),你可以定義陣列有任意的階級(維度數)。 資料可以在任意維度中大小不一。

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 Class指定了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 建立一個與前一個範例物件維度相同的物件,但此範例使用物件 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 建構子的資料不會像建 array 構子那樣在 GPU 上複製。 相反地,當執行核心函式時,資料會被複製到加速器。 因此,如果你建立兩個 array_view 使用相同資料的物件,兩個 array_view 物件都會指向相同的記憶體空間。 這樣做時,你必須同步任何多執行緒存取。 使用此 array_view 類別的主要優點是資料僅在必要時才會移動。

陣列與 array_view 的比較

下表總結了 和 array 類別之間的array_view相似與差異。

說明 陣列類別 array_view類別
當軍階確定時 在編譯過程中。 在編譯時。
當範圍確定時 在運行時。 在運行時。
形狀 長方形。 長方形。
數據記憶體 是一個資料容器。 是一個資料封裝器。
Copy 定義上是明確且深度的複製。 核心函式存取時會自動複製。
資料擷取 方法是將陣列資料複製回 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 明確設定,你可以細緻控制共享記憶體的使用方式,從而根據計算核心的記憶體存取模式,優化應用程式的硬體效能特性。 An array_view 反映的與其所關聯的 是cpu_access_type相同的array;或者,如果 array_view 是沒有資料來源建構的,則access_type反映出最初促使它分配儲存空間的環境。 也就是說,如果它最初是由主機(CPU)存取,那麼它會像是在 CPU 資料來源上建立的,並且共享由捕捉相關的access_typeaccelerator_view;然而,如果最初是由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 運算式定義了每個執行緒上執行的程式碼。 捕獲子句 [=],,指定 lambda 表達式的主體以值存取所有捕捉變數,此處 a為 、 bsum和 。 在此範例中,參數列表產生一個名為 index的一維idx變數。 在第一執行緒中 的 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";
    }
}

加速程式碼:磚塊與障礙

你可以透過鋪磚獲得額外的加速度。 平鋪將線程分割成相等的矩形子集或 圖塊。 你可以根據資料集和你正在編碼的演算法來決定合適的圖塊大小。 對於每個執行緒,你可以存取資料元素相較於整個 or arrayarray_view位置,以及相對於該圖塊的本地位置。 使用區域索引值會簡化程式碼,因為可以避免撰寫程式碼將索引值從全域轉換至區域。 要使用平鋪,請在計算域呼叫 parallel_for_each,並在 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 Data Member 的數值來判斷是否支持雙精度。 快速數學函式庫位於 Concurrency::fast_math 命名空間中,包含另一組數學函數。 這些函式只支援 float 運算元,執行速度較快,但精度不如雙精度數學函式庫中的函式。 函式包含在 <amp_math.h> 標頭檔中,且皆以 宣告。restrict(amp) cmath< 標頭檔案中的>函式會匯入 fast_mathprecise_math 命名空間。 restrict 關鍵字用來區分 <cmath> 版本與 C++ AMP 版本。 以下程式碼利用快速方法計算每個計算域內值的進位十對數。

#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 類別:你可以用材質類別從記憶體或檔案中建立材質。 紋理類似陣列,因為它們包含資料,且在指派與複製建構上類似 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++加速大規模平行處理原則)
Lambda 表達式語法
參考 (C++ AMP)
原生代碼中的平行程式設計部落格