Compartilhar via


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 acesso tile_static. O acesso aos dados na memória tile_static pode ser significativamente mais rápido do que o acesso a dados no espaço global (objetos array ou array_view). Uma instância de uma variável tile_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ória tile_static uma vez da memória global e, em seguida, acessados muitas vezes da memória tile_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 para tile_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 a tile_barrier::wait, até que todos os threads tenham atingido a chamada. Isso significa que, usando o método tile_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ória tile_static do bloco inteiro seguido de uma chamada para tile_barrier::wait. O código após tile_barrier::wait contém cálculos que exigem acesso a todos os valores tile_static.

  • Indexação local e global. Você tem acesso ao índice do thread em relação ao objeto array_view ou array 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áveis tile_static e indexação global para acessar variáveis array e array_view.

  • Classe tiled_extent e classe tiled_index. Use um objeto tiled_extent em vez de um objeto extent na chamada parallel_for_each. Use um objeto tiled_index em vez de um objeto index na chamada parallel_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.

Diagram of an 8 by 9 matrix divided into 2 by 3 tiles.

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.

  1. O vetor das estruturas Description é copiado em um objeto array_view 8x9.

  2. O método parallel_for_each é chamado com um objeto tiled_extent como o domínio de computação. O objeto tiled_extent é criado chamando o método extent::tile() da variável descriptions. Os parâmetros de tipo da chamada para extent::tile(), <2,3> especificam que blocos 2x3 são criados. Portanto, a matriz 8x9 é dividida em 12 blocos, quatro linhas e três colunas.

  3. O método parallel_for_each é chamado usando um objeto tiled_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>()).

  4. Quando cada thread é executado, o índice t_idx retorna informações sobre em qual bloco o thread está (propriedade tiled_index::tile) e o local do thread dentro do bloco (propriedade tiled_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:

  1. O rawData é armazenado em uma matriz 8x8.

  2. 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 classe array é uma delas.

  3. O tamanho da matriz e o tamanho da amostra são definidos usando instruções #define, pois os parâmetros de tipo para array, array_view, extent e tiled_index devem ser valores constantes. Você também pode usar declarações const int static. Como benefício adicional, é trivial alterar o tamanho da amostra para calcular a média acima de blocos 4x4.

  4. 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.

  5. 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 para tile_barrier::wait.

  6. 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 loop for.

  7. Os dados na variável averages, como estão em um objeto array, devem ser copiados de volta no host. Este exemplo usa o operador de conversão de vetor.

  8. 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:

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