Udostępnij za pośrednictwem


Omówienie AMP C++

C++ Accelerated Massive Parallelism (C++ AMP) przyspiesza wykonywanie kodu C++ wykorzystując sprzęt obsługujący dane równoległe, takie jak jednostka przetwarzania grafiki (GPU) na dyskretną kartę graficzną.Używając C++ AMP można kodować algorytmy wielowymiarowych danych, dzięki czemu można przyśpieszyć wykonywanie za pomocą równoległości na heterogenicznym sprzęcie.Model programowania C++ AMP zawiera tablice wielowymiarowe, indeksowanie, transfer pamięci, układanie i bibliotekę funkcji matematycznych.Można użyć rozszerzeń języka C++ AMP do kontroli sposobu przenoszenia danych z procesora CPU do GPU i z powrotem, dzięki czemu można zwiększyć wydajność.

Wymagania systemowe

  • Windows 7, Windows 8, Windows Server 2008 R2 lub Windows Server 2012

  • DirectX 11 funkcja poziom 11.0 lub nowszego sprzętu

  • Do debugowania na emulatorze oprogramowania wymagane są Windows 8 lub Windows Server 2012.Do debugowania na sprzęcie, należy zainstalować sterowniki dla karty graficznej.Aby uzyskać więcej informacji, zobacz Debugowanie kodu GPU.

Wprowadzenie

Dwa poniższe przykłady ilustrują podstawowe składniki C++ AMP.Zakładając, że chcesz dodać odpowiadające elementy dwóch tablic jednowymiarowych.Na przykład, można dodać {1, 2, 3, 4, 5} i {6, 7, 8, 9, 10} aby uzyskać {7, 9, 11, 13, 15}.Bez korzystania z C++ AMP można napisać następujący kod, służący do dodawania liczb i wyświetlić wyniki.

#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";
    }
}

Następujące części kodu są istotne:

  • Dane: Dane składają się z trzech tablic.Wszystkie mają te same rangi (jeden) i długości (pięć).

  • Iteracja: Pierwsza pętla for udostępnia mechanizm do iteracji na elementach w tablicach.Kod, który chcesz wykonać do obliczenia sum jest zawarty w pierwszym bloku for.

  • Indeks: Zmienna idx uzyskuje dostęp do poszczególnych elementów tablic.

Używając C++ AMP, można napisać poniższy kod zamiast kodu podanego w poprzednim przykładzie.

#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";
    }
}

Obecne są te same elementy podstawowe, ale użyte są konstrukcje C++ AMP:

  • Dane: Używasz tablic C++ do konstruowania trzech obiektów AMP C++ array_view.Należy dostarczyć czterech wartości do konstruowania obiektu array_view: wartości danych, ranking, typ elementu i długość obiektu array_view, w każdym wymiarze.Ranga i typ są przekazywane jako parametry typu.Dane i długość są przekazywane jako parametry konstruktora.W tym przykładzie, tablica C++, która jest przekazywana do konstruktora, jest jednowymiarowa.Ranga i długość są używane do konstruowania prostokątnego kształtu danych w obiekcie array_view a wartości danych są używane do wypełnienia tablicy.Biblioteka wykonawcza obejmuje również Array, klasa, która posiada interfejs podobny do klasy array_view i jest omówiona w dalszej części tego artykułu.

  • Iteracja: parallel_for_each funkcji (C++ AMP) dostarcza mechanizm do iteracji na elementach danych lub domenę obliczeniową.W tym przykładzie domena obliczeniowa jest określona przez sum.extent.Kod, który chcesz wykonać zawarty jest w wyrażeniu lambda lub w funkcji jądra.restrict(amp) wskazuje, że używany jest tylko podzbiór języka C++, który C++ AMP może przyspieszyć.

  • Indeks: Zmienna Indeks klasy, idx, jest zadeklarowana z rangą równą jeden, aby odpowiadać randze obiektu array_view.Używając indeksu można uzyskać dostęp do poszczególnych elementów obiektów array_view.

Formowanie i indeksowanie danych: indeks i zakres

Należy zdefiniować wartości danych i zadeklarować kształt danych przed uruchomieniem kodu jądra.Wszystkie dane są zdefiniowane jako tablica (prostokątna) i można zdefiniować tablicę o każdej randze (liczba wymiarów).Dane mogą być dowolnego rozmiaru, w dowolnym z wymiarów.

Hh265136.collapse_all(pl-pl,VS.110).gifIndeks klasy

Indeks klasy określa lokalizację w obiekcie array lub array_view poprzez hermetyzację przesunięcia z miejsca pochodzenia w każdym wymiarze, do jednego obiektu.Po uzyskaniu dostępu do lokalizacji w tablicy, należy przekazać obiekt index do operatora indeksowania [], zamiast listy indeksów liczb całkowitych.Do elementów każdego wymiaru, można uzyskać dostęp za pomocą Operator Array::operator() lub Operator array_view::operator().

Poniższy przykład tworzy jednowymiarowy indeks, który określa trzeci element w jednowymiarowym obiekcie array_view.Indeks jest używany do drukowania trzeciego elementu w obiekcie array_view.Dane wyjściowe to 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

Poniższy przykład tworzy dwuwymiarowy indeks, który określa element, gdzie wiersz = 1 i kolumna = 2, w dwuwymiarowym obiekcie array_view.Pierwszy parametr w konstruktorze index jest składnikiem wiersza, a drugi parametr jest składnikiem kolumny.Dane wyjściowe to 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

Poniższy przykład tworzy trójwymiarowy indeks, który określa element gdzie głębokość = 0, wiersz = 1 i kolumna = 3, w trójwymiarowym obiekcie array_view.Należy zwrócić uwagę na to, że pierwszy parametr jest składnikiem głębokości, drugi parametr jest składnikiem wiersza a trzeci parametr jest składnikiem kolumny.Dane wyjściowe to 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

Hh265136.collapse_all(pl-pl,VS.110).gifzakres klasy

zakres klasy (C++ AMP) określa długość danych, w każdym wymiarze, dla obiektu array lub array_view.Można utworzyć zakres i użyć go do utworzenia obiektu array lub array_view.Można także pobrać zakres istniejącego obiektu array lub array_view.Poniższy przykład drukuje długość zakresu w każdym wymiarze obiektu 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";

Poniższy przykład tworzy obiekt array_view, który ma takie same wymiary jak obiekt w poprzednim przykładzie, ale w tym przykładzie użyty jest obiekt extent zamiast używania jawnych parametrów w konstruktorze 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";

Przenoszenie danych do akceleratora: array i array_view

W bibliotece wykonawczej są zdefiniowane dwa kontenery danych, używane do przenoszenia danych do akceleratora.Są to Array, klasa i Klasa array_view.Klasa array jest klasą kontenera, która tworzy głęboką kopię danych, gdy obiekt jest konstruowany.Klasa array_view jest klasą otoki, która kopiuje dane, gdy funkcja jądra uzyskuje dostęp do danych.Gdy dane są potrzebne na urządzeniu źródłowym, dane są kopiowane z powrotem.

Hh265136.collapse_all(pl-pl,VS.110).gifklasa Array

Gdy obiekt array jest konstruowany, tworzona jest głęboka kopia danych na akceleratorze, jeżeli został użyty konstruktor, który zawiera wskaźnik do zestawu danych.Funkcja jądra modyfikuje kopię na akceleratorze.Po zakończeniu wykonywania funkcji jądra, należy skopiować dane do struktury źródła danych.Poniższy przykład mnoży każdy element wektora przez 10.Po zakończeniu funkcji jądra, operator konwersji wektora jest używany do kopiowania danych do obiektu wektora.

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";
}

Hh265136.collapse_all(pl-pl,VS.110).gifKlasa array_view

array_view ma prawie te same elementy członkowskie co klasa array, ale jej zachowanie podstawowe, nie jest takie same.Dane przekazywane do konstruktora array_view nie są replikowane na GPU, tak jak w konstruktorze array.Zamiast tego, dane są kopiowane do akceleratora po wykonaniu funkcji jądra.Dlatego, jeśli utworzysz dwa obiekty array_view, które używają tych samych danych, oba obiekty array_view odnoszą się do tego samego obszaru pamięci.Po wykonaniu tej czynności, należy zsynchronizować każdy dostęp do wielu wątków.Główną zaletą używania klasy array_view jest to, że dane są przenoszone tylko wtedy, gdy jest to konieczne.

Hh265136.collapse_all(pl-pl,VS.110).gifPorównanie klas array i array_view

W poniższej tabeli zestawiono podobieństwa i różnice między klasami array i array_view.

Opis

Klasa array

Klasa array_view

Kiedy ustalana jest ranga

W czasie kompilacji.

W czasie kompilacji.

Kiedy określany jest zakres

W czasie wykonywania.

W czasie wykonywania.

Kształt

Prostokątny.

Prostokątny.

Przechowywanie danych

Jest kontenerem danych.

Jest otoką danych.

Kopiuj

Definicja jawnego i głębokiego kopiowania.

Niejawne kopiowanie, gdy dostęp uzyskuje funkcja jądra.

Pobieranie danych

Przez skopiowanie danych z tablicy, z powrotem do obiektu w wątku procesora.

Przez bezpośredni dostęp do obiektu array_view lub przez wywołanie metody Metoda array_view::synchronize do dalszego uzyskiwania dostępu do danych w oryginalnym kontenerze.

Wykonywanie kodu na danych: parallel_for_each

Funkcja parallel_for_each definiuje kod, który należy uruchomić na akceleratorze przeciwko danym w obiekcie array lub array_view.Należy rozważyć następujący kod, znajdujący się w sekcji wprowadzenia, w tym temacie.

#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";
    }
}

Metoda parallel_for_each przyjmuje dwa argumenty, domenę obliczeniową i wyrażenie lambda.

Domena obliczeniowa jest obiektem extent lub obiektem tiled_extent, który definiuje zestaw wątków do utworzenia dla przetwarzania równoległego.Jeden wątek jest generowany dla każdego elementu, w domenie obliczeniowej.W tym przypadku, obiekt extent jest jednowymiarowy i zawiera pięć elementów.Dlatego też, uruchamianych jest pięć wątków.

Wyrażenie lambda definiuje kod, do uruchomienia w każdym wątku.Klauzula przechwytywania [=] określa, że ciało wyrażenia lambda ma dostęp do wszystkich przechwyconych zmiennych według wartości, które w tym przypadku to a, b i sum.W tym przykładzie lista parametrów tworzy jednowymiarową zmienną index o nazwie idx.Wartość idx[0] w pierwszym wątku wynosi 0 i zwiększa się o jeden w każdym kolejnym wątku.restrict(amp) wskazuje, że używany jest tylko podzbiór języka C++, który C++ AMP może przyspieszyć.Ograniczenia funkcji, które mają modyfikator ograniczeń, opisane są w Klauzula ograniczenia (C++ AMP).Aby uzyskać więcej informacji, zobacz Składni wyrażenia lambda.

Wyrażenie lambda może zawierać kod do wykonania lub może wywołać oddzielną funkcję jądra.Funkcja jądra musi zawierać modyfikator restrict(amp).Poniższy przykład jest równoważny z poprzednim przykładem, ale wywołuje oddzielną funkcję jądra.

#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";
    }
}

Kod przyspieszenia: Fragmenty i bariery

Dodatkowe przyspieszenie można uzyskać za pomocą fragmentowania.Fragmentowanie dzieli wątki na równe, prostokątne podzestawy lub fragmenty.Można określić odpowiedni rozmiar fragmentu, na podstawie zestawu danych i algorytmu, który jest są kodowany.Dla każdego wątku można mieć dostęp do lokalizacji globalnej elementu danych, w stosunku do całości array lub array_view i dostęp do lokalizacji lokalnej względem fragmentu.Używanie lokalnej wartości indeksu upraszcza kod, ponieważ nie trzeba pisać kodu do przekształcania wartości indeksu, z globalnej na lokalną.W celu użycia fragmentowania, należy wywołać Metoda Extent::Tile w domenie obliczeniowej, w metodzie parallel_for_each i wykorzystać obiekt tiled_index w wyrażeniu lambda.

W typowych aplikacjach, elementy we fragmencie są powiązane w jakiś sposób, a kod musi mieć dostęp i śledzić wartości w całym fragmencie.Użyj słowa kluczowego tile_static słowa kluczowego oraz Metoda tile_barrier::wait aby to osiągnąć.Zmienna, która ma słowo kluczowe tile_static ma zakres na całym fragmencie, a wystąpienie zmiennej jest tworzone dla każdego fragmentu.Należy obsłużyć synchronizację dostępu, wątków fragmentu, do zmiennej.Metoda tile_barrier::wait zatrzymuje wykonywanie bieżącego wątku, aż wszystkie wątki we fragmencie osiągną wywołanie tile_barrier::wait.A więc, można zbierać wartości z całego fragmentu, za pomocą zmiennych tile_static.Następnie można zakończyć wszelkie obliczenia, które wymagają dostępu do wszystkich wartości.

Poniższy diagram przedstawia dwuwymiarową tablicę danych próbkowania, które są ułożone we fragmenty.

Wartości indeksów w zakresie rozmieszczony sąsiadująco

Poniższy przykład kodu używa danych próbkowania, z poprzedniego diagramu.Kod zastępuje każdą wartość we fragmencie, średnią wartości zawartych we fragmencie.

// 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

Biblioteki funkcji matematycznych

C++ AMP zawiera dwie biblioteki funkcji matematycznych.Biblioteka podwójnej precyzji w Obszar nazw CONCURRENCY::precise_math zapewnia obsługę funkcji podwójnej precyzji.Dostarcza również wsparcie dla funkcji pojedynczej precyzji, chociaż nadal wymagane jest wsparcie sprzętu, dla podwójnej precyzji.Jest to zgodne ze Specyfikacją C99 (ISO/IEC 9899).Akcelerator musi w pełni obsługiwać podwójną precyzję.Można określić, czy jest ona spełniona, przez sprawdzenie wartości dla Accelerator::supports_double_precision Członkowskich danych.Szybka biblioteka funkcji matematycznych, w Obszar nazw CONCURRENCY::fast_math, zawiera inny zestaw funkcji matematycznych.Te funkcje, które obsługują tylko operatory typu float, wykonują się dużo szybciej, ale nie są tak dokładne jak te w bibliotece funkcji matematycznych podwójnej precyzji.Funkcje są zawarte w pliku nagłówka <amp_math.h> i wszystkie są zadeklarowane jako restrict(amp).Funkcje w pliku nagłówka <cmath> są importowane do obu obszarów nazw fast_math i precise_math.Słowo kluczowe restrict jest używane do odróżnienia wersji <cmath> od wersji C++ AMP. Poniższy kod oblicza logarytm dziesiętny, przy użyciu szybkiej metody, dla każdej wartości znajdującej się w domenie obliczeniowej.

#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";
    }
}

Biblioteka Graficzna

C++ AMP zawiera bibliotekę graficzną, która jest przeznaczona do przyspieszonego programowania grafiki.Ta biblioteka jest używana tylko na urządzeniach, które obsługują macierzystą funkcjonalność graficzną.Metody znajdują się w Obszar nazw CONCURRENCY::Graphics i są zawarte w pliku nagłówka <amp_graphics.h>.Kluczowe składniki biblioteki graficznej to:

  • Tekstura, klasa: Można użyć klasy texture, do utworzenia tekstur z pamięci lub pliku.Tekstury przypominają tablice, ponieważ zawierają one dane i przypominają kontenery w Standard Template Library (STL) w odniesieniu do konstrukcji przydziału i kopiowania.Aby uzyskać więcej informacji, zobacz Pojemniki STL.Parametry szablonu dla klasy texture są typami elementu i rangi.Ranga może wynosić 1, 2 lub 3.Typ elementu może być jednym z typów krótkich wektorów, które są opisane w dalszej części tego artykułu.

  • Klasa writeonly_texture_view: Zapewnia dostęp, tylko do zapisu, do wszelkich tekstur.

  • Krótkie biblioteki wektorowe: Definiuje zestaw typów krótkich wektorów, o długości 2, 3 i 4, opartych na int, uint, float, double, norm lub unorm.

Windows StoreAplikacje

Podobnie jak inne biblioteki C++, można użyć C++ AMP, a aplikacjach Windows Store.Artykuły te opisują jak dołączyć kod C++ AMP w aplikacjach utworzonych za pomocą języka C++, C#, Visual Basic lub języka JavaScript:

C++ AMP i Concurrency Visualizer

Concurrency Visualizer zawiera obsługę dla analizowania wydajności kodu C++ AMP.Artykuły te opisują następujące funkcje:

Zalecenia dotyczące wydajności

Wyznaczanie modułu i dzielenie liczb całkowitych bez znaku, mają znacznie lepszą wydajność niż wyznaczanie modułu i dzielenie liczb całkowitych ze znakiem.Zalecane jest, aby korzystać z liczb całkowitych bez znaku, gdy jest to możliwe.

Zobacz też

Informacje

Składni wyrażenia lambda

Inne zasoby

C++ AMP (C++ przyspieszyć równoległości prostych masowe)

Odwołanie (C++ AMP)

Programowanie równoległe w Native Code Blog