C++ AMP 概觀
C++ Accelerated Massive Parallelism (C++ AMP) 會利用資料平行硬體 (例如獨立圖形顯示卡上的圖形處理器 (GPU)) 來加速 C++ 程式碼的執行。 使用 C++ AMP,您可以撰寫多維資料演算法的程式碼,利用異質硬體上的平行處理加速執行。 C++ AMP 程式撰寫模型包含多維陣列、索引、記憶體傳輸、磚和數學函式庫。 您可以使用 C++ AMP 語言擴充功能控制資料從 CPU 移至 GPU 再移回的方式,讓您可以改善效能。
系統需求
Windows 7、Windows 8、Windows Server 2008 R2 或 Windows Server 2012
如需在軟體模擬器上偵錯,需要使用 Windows 8 或 Windows Server 2012。 如需在硬體上偵錯,您必須安裝圖形卡的驅動程式。 如需詳細資訊,請參閱偵錯 GPU 程式碼。
簡介
下列兩個範例說明 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";
}
}
程式碼的重點如下:
資料:資料包含三個陣列。 全部具有相同的陣序 (1) 和長度 (5)。
反覆項目:第一個 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 運算式或「核心功能」(Kernel Function) 中。 restrict(amp) 表示,只會使用 C++ AMP 可以加速的 C++ 語言子集。
索引: index 類別 變數 idx 的宣告層級符合 array_view 物件的層級。 您可以使用索引,存取 array_view 物件的個別項目。
定形和索引資料:索引和範圍
您必須先定義資料值和宣告資料的圖形,才能執行核心程式碼。 所有資料都已定義為陣列 (Rectangle),然後,您可以定義陣列具有任何陣序 (維度數目)。 資料可以是任何維度中的任何大小。
index 類別
index 類別 會在 array 或 array_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 類別 (C++ AMP) 會指定 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 類別 和 array_view 類別。 array 類別是在建構物件時建立資料的深層複本 (Deep Copy) 的容器類別。 array_view 類別是包裝函式類別,會在核心功能存取資料時複製資料。 當來源裝置需要資料時,會將資料複製回來。
array 類別
當 array 物件已建構時,如果您使用包含資料集指標的建構函式,將會在加速器上建立資料的深層複本 (Deep Copy)。 核心功能會修改加速器上的複本。 當核心功能執行完成時,您必須將資料複製回到來源資料結構。 下列範例會將向量中的每個項目乘以 10。 核心功能完成後,使用向量轉換運算子將資料複製回向量物件。
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 和 array_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 屬性決定。 根據預設, array 和 array_view 物件和主要關聯的 accelerator 採用相同的 access_type 。
藉由明確設定 array 的 array::cpu_access_type 資料成員 屬性,您可以對共用記憶體使用方式進行細部控制,以便根據計算核心的記憶體存取模式最佳化硬體效能特性的應用程式。 array_view 會反映其相關聯之 array 的 cpu_access_type;或者若陣列檢視是在沒有資料來源的情況下建構的,其 access_type 會反映第一個使其配置儲存的環境。 也就是說,如果先由主機 (CPU) 存取,則其行為就如同是透過 CPU 資料來源所建立,並共用藉由擷取關聯之 accelerator_view 的 access_type。不過,如果先由 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 運算式。
「計算網域」(Compute Domain) 是 extent 物件或 tiled_extent 物件,用於定義針對平行執行所建立的執行緒。 計算網域的每個項目都會產生一個執行緒。 在這個案例中, extent 物件是一維的且具有五個項目。 因此,五個執行緒都會啟動。
「Lambda 運算式」(Lambda Expression) 負責定義在每個執行緒上執行的程式碼。 擷取子句 [=] 會指定,Lambda 運算式的主體會依照值存取所有擷取的變數,在這個案例中為 a、b 和 sum。 在此範例中,參數清單會建立名稱為 idx 的一維 index變數。 idx[0] 在第一個執行緒中的值是 0,且後續每個執行緒都會增加一。 restrict(amp) 表示,只會使用 C++ AMP 可以加速的 C++ 語言子集。限制子句 (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";
}
}
加速程式碼:並排和屏障
您可以使用並排,取得額外的加速效果。 並排分割會將執行緒劃分成相等的矩形子集或「Tile」。 您可以根據資料集和您撰寫的演算法程式碼,判斷正確的並排大小。 對於每個執行緒,您可以存取資料項目相對於整個 array 或 array_view 的「全域」(Global) 位置,並存取相對於 Tile 的「區域」(Local) 位置。 使用區域索引值會簡化程式碼,因為您不必撰寫程式碼,將索引值從全域轉譯為區域。 若要使用並排,請在 parallel_for_each 方法之計算網域中呼叫 extent::tile 方法,並在 Lambda 運算式中使用 tiled_index 物件。
在一般的應用程式中,tile 中的元素具有某種相關性,而且程式碼必須存取和記錄跨 tile 的值。 使用 tile_static 關鍵字 關鍵字和 tile_barrier::wait 方法 完成這項工作。 具有範圍跨整個 Tile 之 tile_static 關鍵字的變數,而且針對每個 Tile 建立變數的執行個體。 您必須處理變數的並排-執行緒存取同步處理。 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 資料成員 的值來判斷是否如此。 Concurrency::fast_math 命名空間 中的 fast math 程式庫包含另一組數學函式。 這些函式只支援 float 運算元,可以更快速地執行,但其精確度不如雙精確度數學程式庫中的函式。 函式包含在 <amp_math.h> 標頭檔中,而且全都以 restrict(amp) 宣告。 在 <cmath> 標頭檔中的函式會同時匯入至 fast_math 和 precise_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(logs[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
圖庫
C++ AMP 包含為了加速圖形程式設計所設計的圖形程式庫。 這個程式庫只有在支援原生繪圖功能的裝置上使用。 方法位於 Concurrency::graphics 命名空間 中,且包含在 <amp_graphics.h> 標頭檔中。 圖庫的主要元件是:
texture 類別:您可以使用紋理類別從記憶體或檔案建立紋理。 紋理類似陣列,因為它們包含資料,而且在指派和複製建構方面類似標準樣板程式庫 (STL) 中的容器。 如需詳細資訊,請參閱STL 容器。 texture 類別的樣板參數是元素類型和順位。 順位可以是 1、2 或 3。 項目類型可以是本文稍後描述的其中一種短向量類型。
writeonly_texture_view 類別:提供所有材質的唯讀存取。
簡短的向量文件庫:定義以 int、uint、float、double、unorm 或 unorm 為基礎的一組長度為 2、3 和 4 的短向量類型。
Windows 市集 應用程式
就像其他 C++ 程式庫,您可以在 Windows 市集 應用程式使用 C++ AMP。 這些文件描述如何將 C++ AMP 程式碼加入使用 C++、C#、Visual Basic 或 JavaScript 建立的應用程式中:
如何從 C# 中使用 C++ AMP (英文)
C++ AMP 和並行視覺化檢視
並行視覺化檢視支援分析 C++ AMP 程式碼的效能。 這些文件說明下列功能:
效能建議
不帶正負號的整數模數和除法的效能比帶正負號的整數模數和除法的效能大。 建議您盡可能使用不帶正負號的整數。