Usando blocos
Você pode usar blocos para maximizar a aceleração do seu aplicativo. O bloco divide os threads em subconjuntos retangulares iguais ou blocos. Se você usar um tamanho de bloco apropriado e um algoritmo em bloco, poderá obter ainda mais aceleração do código C++ AMP. Os componentes básicos do bloco são:
Variáveis
tile_static
. O principal benefício do bloco é o ganho de desempenho devido ao acessotile_static
. O acesso aos dados na memóriatile_static
pode ser significativamente mais rápido do que o acesso a dados no espaço global (objetosarray
ouarray_view
). Uma instância de uma variáveltile_static
é criada para cada bloco e todos os threads no bloco têm acesso à variável. Em um algoritmo de bloco típico, os dados são copiados na memóriatile_static
uma vez da memória global e, em seguida, acessados muitas vezes da memóriatile_static
.Método tile_barrier::wait. Uma chamada para
tile_barrier::wait
suspende a execução do thread atual até que todos os threads no mesmo bloco cheguem à chamada paratile_barrier::wait
. Você não pode garantir a ordem em que os threads serão executados, apenas que nenhum thread no bloco será executado após a chamada atile_barrier::wait
, até que todos os threads tenham atingido a chamada. Isso significa que, usando o métodotile_barrier::wait
, você pode executar tarefas bloco por bloco, em vez de thread por thread. Um algoritmo de bloco típico tem código para inicializar a memóriatile_static
do bloco inteiro seguido de uma chamada paratile_barrier::wait
. O código apóstile_barrier::wait
contém cálculos que exigem acesso a todos os valorestile_static
.Indexação local e global. Você tem acesso ao índice do thread em relação ao objeto
array_view
ouarray
inteiro e ao índice relativo ao bloco. Usando o índice local, você pode facilitar a leitura e a depuração do código. Normalmente, você usa a indexação local para acessar variáveistile_static
e indexação global para acessar variáveisarray
earray_view
.Classe tiled_extent e classe tiled_index. Use um objeto
tiled_extent
em vez de um objetoextent
na chamadaparallel_for_each
. Use um objetotiled_index
em vez de um objetoindex
na chamadaparallel_for_each
.
Para aproveitar o bloco, o algoritmo deve particionar o domínio da computação em blocos e copiar os dados do bloco em variáveis tile_static
para acesso mais rápido.
Exemplo de índices globais, de blocos e locais
Observação
Os cabeçalhos AMP C++ foram preteridos a partir do Visual Studio 2022 versão 17.0.
Incluir todos os cabeçalhos AMP gerará erros de build. Defina _SILENCE_AMP_DEPRECATION_WARNINGS
antes de incluir qualquer cabeçalho AMP para silenciar os avisos.
O diagrama a seguir representa uma matriz 8x9 de dados que é organizada em blocos 2x3.
O exemplo a seguir exibe os índices globais, de blocos e locais dessa matriz de blocos. Um objeto array_view
é criado usando elementos do tipo Description
. O Description
contém os índices globais, de blocos e locais do elemento na matriz. O código na chamada para parallel_for_each
define os valores dos índices globais, de blocos e locais de cada elemento. A saída exibe os valores nas estruturas Description
.
#include <iostream>
#include <iomanip>
#include <Windows.h>
#include <amp.h>
using namespace concurrency;
const int ROWS = 8;
const int COLS = 9;
// tileRow and tileColumn specify the tile that each thread is in.
// globalRow and globalColumn specify the location of the thread in the array_view.
// localRow and localColumn specify the location of the thread relative to the tile.
struct Description {
int value;
int tileRow;
int tileColumn;
int globalRow;
int globalColumn;
int localRow;
int localColumn;
};
// A helper function for formatting the output.
void SetConsoleColor(int color) {
int colorValue = (color == 0) 4 : 2;
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colorValue);
}
// A helper function for formatting the output.
void SetConsoleSize(int height, int width) {
COORD coord;
coord.X = width;
coord.Y = height;
SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coord);
SMALL_RECT* rect = new SMALL_RECT();
rect->Left = 0;
rect->Top = 0;
rect->Right = width;
rect->Bottom = height;
SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE), true, rect);
}
// This method creates an 8x9 matrix of Description structures.
// In the call to parallel_for_each, the structure is updated
// with tile, global, and local indices.
void TilingDescription() {
// Create 72 (8x9) Description structures.
std::vector<Description> descs;
for (int i = 0; i < ROWS * COLS; i++) {
Description d = {i, 0, 0, 0, 0, 0, 0};
descs.push_back(d);
}
// Create an array_view from the Description structures.
extent<2> matrix(ROWS, COLS);
array_view<Description, 2> descriptions(matrix, descs);
// Update each Description with the tile, global, and local indices.
parallel_for_each(descriptions.extent.tile< 2, 3>(),
[=] (tiled_index< 2, 3> t_idx) restrict(amp)
{
descriptions[t_idx].globalRow = t_idx.global[0];
descriptions[t_idx].globalColumn = t_idx.global[1];
descriptions[t_idx].tileRow = t_idx.tile[0];
descriptions[t_idx].tileColumn = t_idx.tile[1];
descriptions[t_idx].localRow = t_idx.local[0];
descriptions[t_idx].localColumn= t_idx.local[1];
});
// Print out the Description structure for each element in the matrix.
// Tiles are displayed in red and green to distinguish them from each other.
SetConsoleSize(100, 150);
for (int row = 0; row < ROWS; row++) {
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Value: " << std::setw(2) << descriptions(row, column).value << " ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Tile: " << "(" << descriptions(row, column).tileRow << "," << descriptions(row, column).tileColumn << ") ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Global: " << "(" << descriptions(row, column).globalRow << "," << descriptions(row, column).globalColumn << ") ";
}
std::cout << "\n";
for (int column = 0; column < COLS; column++) {
SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
std::cout << "Local: " << "(" << descriptions(row, column).localRow << "," << descriptions(row, column).localColumn << ") ";
}
std::cout << "\n";
std::cout << "\n";
}
}
int main() {
TilingDescription();
char wait;
std::cin >> wait;
}
O trabalho principal do exemplo está na definição do objeto array_view
e na chamada para parallel_for_each
.
O vetor das estruturas
Description
é copiado em um objetoarray_view
8x9.O método
parallel_for_each
é chamado com um objetotiled_extent
como o domínio de computação. O objetotiled_extent
é criado chamando o métodoextent::tile()
da variáveldescriptions
. Os parâmetros de tipo da chamada paraextent::tile()
,<2,3>
especificam que blocos 2x3 são criados. Portanto, a matriz 8x9 é dividida em 12 blocos, quatro linhas e três colunas.O método
parallel_for_each
é chamado usando um objetotiled_index<2,3>
(t_idx
) como o índice. Os parâmetros de tipo do índice (t_idx
) devem corresponder aos parâmetros de tipo do domínio de computação (descriptions.extent.tile< 2, 3>()
).Quando cada thread é executado, o índice
t_idx
retorna informações sobre em qual bloco o thread está (propriedadetiled_index::tile
) e o local do thread dentro do bloco (propriedadetiled_index::local
).
Tile Synchronization—tile_static and tile_barrier::wait
O exemplo anterior ilustra o layout e os índices do bloco, mas não é em si muito útil. O bloco se torna útil quando os blocos são integrais ao algoritmo e exploram variáveis tile_static
. Como todos os threads em um bloco têm acesso a variáveis tile_static
, as chamadas a tile_barrier::wait
são usadas para sincronizar o acesso às variáveis tile_static
. Embora todos os threads em um bloco tenham acesso às variáveis tile_static
, não há nenhuma ordem garantida de execução de threads no bloco. O exemplo a seguir mostra como usar variáveis tile_static
e o método tile_barrier::wait
para calcular o valor médio de cada bloco. Aqui estão as chaves para entender o exemplo:
O rawData é armazenado em uma matriz 8x8.
O tamanho do bloco é 2x2. Isso cria uma grade 4x4 de blocos e as médias podem ser armazenadas em uma matriz 4x4 usando um objeto
array
. Há apenas um número limitado de tipos que você pode capturar por referência em uma função restrita por AMP. A classearray
é uma delas.O tamanho da matriz e o tamanho da amostra são definidos usando instruções
#define
, pois os parâmetros de tipo paraarray
,array_view
,extent
etiled_index
devem ser valores constantes. Você também pode usar declaraçõesconst int static
. Como benefício adicional, é trivial alterar o tamanho da amostra para calcular a média acima de blocos 4x4.Uma matriz 2x2
tile_static
de valores flutuantes é declarada para cada bloco. Embora a declaração esteja no caminho do código para cada thread, apenas uma matriz é criada para cada bloco na matriz.Há uma linha de código para copiar os valores em cada bloco para a matriz
tile_static
. Para cada thread, depois que o valor é copiado para a matriz, a execução no thread é interrompida devido à chamada paratile_barrier::wait
.Quando todos os threads em um bloco atingirem a barreira, a média poderá ser calculada. Como o código é executado para cada thread, há uma instrução
if
para calcular apenas a média em um thread. A média é armazenada na variável média. A barreira é essencialmente o constructo que controla cálculos por bloco, tanto quanto você pode usar um loopfor
.Os dados na variável
averages
, como estão em um objetoarray
, devem ser copiados de volta no host. Este exemplo usa o operador de conversão de vetor.No exemplo completo, você pode alterar SAMPLESIZE para 4 e o código é executado corretamente sem nenhuma outra alteração.
#include <iostream>
#include <amp.h>
using namespace concurrency;
#define SAMPLESIZE 2
#define MATRIXSIZE 8
void SamplingExample() {
// Create data and array_view for the matrix.
std::vector<float> rawData;
for (int i = 0; i < MATRIXSIZE * MATRIXSIZE; i++) {
rawData.push_back((float)i);
}
extent<2> dataExtent(MATRIXSIZE, MATRIXSIZE);
array_view<float, 2> matrix(dataExtent, rawData);
// Create the array for the averages.
// There is one element in the output for each tile in the data.
std::vector<float> outputData;
int outputSize = MATRIXSIZE / SAMPLESIZE;
for (int j = 0; j < outputSize * outputSize; j++) {
outputData.push_back((float)0);
}
extent<2> outputExtent(MATRIXSIZE / SAMPLESIZE, MATRIXSIZE / SAMPLESIZE);
array<float, 2> averages(outputExtent, outputData.begin(), outputData.end());
// Use tiles that are SAMPLESIZE x SAMPLESIZE.
// Find the average of the values in each tile.
// The only reference-type variable you can pass into the parallel_for_each call
// is a concurrency::array.
parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
[=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
{
// Copy the values of the tile into a tile-sized array.
tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];
// Wait for the tile-sized array to load before you calculate the average.
t_idx.barrier.wait();
// If you remove the if statement, then the calculation executes for every
// thread in the tile, and makes the same assignment to averages each time.
if (t_idx.local[0] == 0 && t_idx.local[1] == 0) {
for (int trow = 0; trow < SAMPLESIZE; trow++) {
for (int tcol = 0; tcol < SAMPLESIZE; tcol++) {
averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
}
}
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE * SAMPLESIZE);
}
});
// Print out the results.
// You cannot access the values in averages directly. You must copy them
// back to a CPU variable.
outputData = averages;
for (int row = 0; row < outputSize; row++) {
for (int col = 0; col < outputSize; col++) {
std::cout << outputData[row*outputSize + col] << " ";
}
std::cout << "\n";
}
// Output for SAMPLESIZE = 2 is:
// 4.5 6.5 8.5 10.5
// 20.5 22.5 24.5 26.5
// 36.5 38.5 40.5 42.5
// 52.5 54.5 56.5 58.5
// Output for SAMPLESIZE = 4 is:
// 13.5 17.5
// 45.5 49.5
}
int main() {
SamplingExample();
}
Condições de corrida
Pode ser tentador criar uma variável tile_static
nomeada total
e incrementar essa variável para cada thread, do seguinte modo:
// Do not do this.
tile_static float total;
total += matrix[t_idx];
t_idx.barrier.wait();
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);
O primeiro problema com essa abordagem é que as variáveis tile_static
não podem ter inicializadores. O segundo problema é que há uma condição de corrida na atribuição a total
, porque todos os threads no bloco têm acesso à variável em nenhuma ordem específica. Você pode programar um algoritmo para permitir que apenas um thread acesse o total em cada barreira, conforme mostrado a seguir. No entanto, essa solução não é extensível.
// Do not do this.
tile_static float total;
if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
total = matrix[t_idx];
}
t_idx.barrier.wait();
if (t_idx.local[0] == 0&& t_idx.local[1] == 1) {
total += matrix[t_idx];
}
t_idx.barrier.wait();
// etc.
Limites de memória
Há dois tipos de acessos de memória que devem ser sincronizados: acesso global à memória e acesso à memória tile_static
. Um objeto concurrency::array
aloca apenas a memória global. Um concurrency::array_view
pode fazer referência à memória global, à memória tile_static
ou ambas, dependendo de como ela foi construída. Há dois tipos de memória que devem ser sincronizadas:
memória global
tile_static
Um limite de memória garante que os acessos à memória estejam disponíveis para outros threads no bloco de thread e que os acessos à memória sejam executados de acordo com a ordem do programa. Para garantir isso, os compiladores e processadores não reordenam leituras e gravações no limite. No C++ AMP, um limite de memória é criado por uma chamada a um destes métodos:
Método tile_barrier::wait: cria um limite em torno das memórias global e
tile_static
.Método tile_barrier::wait_with_all_memory_fence: cria um limite em torno das memórias global e
tile_static
.Método tile_barrier::wait_with_global_memory_fence: cria um limite em torno apenas da memória global.
Método tile_barrier::wait_with_tile_static_memory_fence: cria um limite em torno apenas da memória
tile_static
.
Chamar o limite específico necessário pode aprimorar o desempenho do aplicativo. O tipo de barreira afeta como o compilador e o hardware reordenam as instruções. Por exemplo, se você usar um limite de memória global, ele se aplicará somente a acessos de memória globais e, portanto, o compilador e o hardware poderão reordenar leituras e gravações para variáveis tile_static
nos dois lados do limite.
No próximo exemplo, a barreira sincroniza as gravações em tileValues
, uma variável tile_static
. Neste exemplo, tile_barrier::wait_with_tile_static_memory_fence
é chamado em vez de tile_barrier::wait
.
// Using a tile_static memory fence.
parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
[=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
{
// Copy the values of the tile into a tile-sized array.
tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];
// Wait for the tile-sized array to load before calculating the average.
t_idx.barrier.wait_with_tile_static_memory_fence();
// If you remove the if statement, then the calculation executes
// for every thread in the tile, and makes the same assignment to
// averages each time.
if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
for (int trow = 0; trow <SAMPLESIZE; trow++) {
for (int tcol = 0; tcol <SAMPLESIZE; tcol++) {
averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
}
}
averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);
}
});
Confira também
C++ AMP (C++ Accelerated Massive Parallelism)
Palavra-chave tile_static