備註
從 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指定了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 建立一個與前一個範例物件維度相同的物件,但此範例使用物件 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決定。 預設情況下,array和array_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,並且共享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 運算式定義了每個執行緒上執行的程式碼。 捕獲子句 [=],,指定 lambda 表達式的主體以值存取所有捕捉變數,此處 a為 、 b、 sum和 。 在此範例中,參數列表產生一個名為 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 array 的array_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_math 和 precise_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 的短向量類型,這些類型基於
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++加速大規模平行處理原則)
Lambda 表達式語法
參考 (C++ AMP)
原生代碼中的平行程式設計部落格