Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
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_staticpode ser significativamente mais rápido do que o acesso a dados no espaço global (objetosarrayouarray_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_staticuma 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::waitsuspende 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_staticdo bloco inteiro seguido de uma chamada paratile_barrier::wait. O código apóstile_barrier::waitconté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_viewouarrayinteiro 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_statice indexação global para acessar variáveisarrayearray_view.Classe tiled_extent e classe tiled_index. Use um objeto
tiled_extentem vez de um objetoextentna chamadaparallel_for_each. Use um objetotiled_indexem vez de um objetoindexna 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 C++ AMP 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_view8x9.O método
parallel_for_eaché chamado com um objetotiled_extentcomo 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_idxretorna informações sobre em qual bloco o thread está (propriedadetiled_index::tile) e o local do thread dentro do bloco (propriedadetiled_index::local).
Sincronização de bloco — tile_static e 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,extentetiled_indexdevem 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_staticde 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
ifpara 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 (Paralelismo Massivo Acelerado C++)
Palavra-chave tile_static