Comparteix a través de


Uso de iconos

Puedes usar mosaicos para maximizar la aceleración de la aplicación. El azulejamiento divide los subprocesos en subconjuntos rectangulares iguales o bloques. Si utiliza un tamaño de bloque adecuado y un algoritmo de mosaico, puede obtener aún más aceleración del código de C++ AMP. Los componentes básicos del mosaico son:

  • tile_static variables. La principal ventaja de la división en bloques es la mejora en el rendimiento del tile_static acceso. El acceso a los datos en tile_static memoria puede ser significativamente más rápido que el acceso a los datos del espacio global (array o array_view objetos). Se crea una instancia de la variable tile_static para cada mosaico y todos los subprocesos del mosaico tienen acceso a la variable. En un algoritmo típico de mosaico, los datos se copian una vez de la memoria global en la tile_static memoria, y luego se accede a ellos muchas veces desde la tile_static memoria.

  • tile_barrier::wait Método Una llamada a tile_barrier::wait suspende la ejecución del subproceso actual hasta que todos los subprocesos en la misma tile alcancen la llamada a tile_barrier::wait. No se puede garantizar el orden en el que se ejecutarán los subprocesos, solo que ningún subproceso del icono se ejecutará más allá de la llamada a tile_barrier::wait hasta que todos los subprocesos hayan alcanzado la llamada. Esto significa que, mediante el uso del método tile_barrier::wait, puede realizar tareas de forma individual para cada unidad de procesamiento, en lugar de hacerlo para cada subproceso. Un algoritmo de mosaico típico tiene código para inicializar la memoria de todo el mosaico, seguido de una llamada a tile_barrier::wait. El código siguiente tile_barrier::wait contiene cálculos que requieren acceso a todos los tile_static valores.

  • Indexación local y global. Tiene acceso al índice del hilo en relación con el objeto array_view completo o array, y al índice con respecto a la tesela. El uso del índice local puede facilitar la lectura y depuración del código. Normalmente, se usa la indexación local para acceder a las variables tile_static, y la indexación global para acceder a las variables array y array_view.

  • Clase tiled_extent y Clase tiled_index. Utilizas un tiled_extent objeto en lugar de un extent objeto en la llamada parallel_for_each. Se usa un objeto tiled_index en lugar de un objeto index en la llamada parallel_for_each.

Para aprovechar el uso de tiling, el algoritmo debe dividir el dominio de cálculo en bloques y luego copiar los datos del bloque en variables tile_static para un acceso más rápido.

Ejemplo de índices globales, de tiles y locales

Nota:

Los encabezados de C++ AMP están en desuso a partir de la versión 17.0 de Visual Studio 2022. Si se incluyen encabezados AMP, se generarán errores de compilación. Defina _SILENCE_AMP_DEPRECATION_WARNINGS antes de incluir encabezados AMP para silenciar las advertencias.

El diagrama siguiente representa una matriz de 8x9 de datos organizados en iconos 2x3.

Diagrama de una matriz de 8 por 9 dividida en bloques de 2 por 3.

En el siguiente ejemplo se muestran los índices globales, de mosaico y locales de esta matriz en mosaico. Un array_view objeto se crea mediante elementos de tipo Description. Description contiene los índices globales, de mosaico y locales del elemento de la matriz. El código de la llamada a parallel_for_each establece los valores de los índices globales, de icono y locales de cada elemento. La salida muestra los valores en las estructuras 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;
}

El trabajo principal del ejemplo se encuentra en la definición del array_view objeto y la llamada a parallel_for_each.

  1. El vector de Description estructuras se copia en un objeto 8x9 array_view .

  2. Se llama al parallel_for_each método con un tiled_extent objeto como dominio de proceso. El tiled_extent objeto se crea llamando al extent::tile() método de la descriptions variable . Los parámetros de tipo de la llamada a extent::tile(), <2,3>especifican que se crean iconos 2x3. Por lo tanto, la matriz 8x9 se divide en mosaicos de 12, cuatro filas y tres columnas.

  3. El método parallel_for_each se llama mediante tiled_index<2,3> un objeto (t_idx) como índice. Los parámetros de tipo del índice (t_idx) deben coincidir con los parámetros de tipo del dominio de proceso (descriptions.extent.tile< 2, 3>()).

  4. Cuando se ejecuta cada subproceso, el índice t_idx devuelve información sobre el icono que el subproceso está en (tiled_index::tile propiedad) y la ubicación del subproceso dentro del icono (tiled_index::local propiedad).

Sincronización de iconos: tile_static y tile_barrier::wait

En el ejemplo anterior se muestra el diseño de mosaico y los índices, pero no es muy útil. La disposición de mosaicos resulta útil cuando los mosaicos son integrales para el algoritmo y explotan las variables tile_static. Dado que todos los subprocesos de un icono tienen acceso a tile_static variables, las llamadas a tile_barrier::wait se usan para sincronizar el acceso a las tile_static variables. Aunque todos los subprocesos de un icono tienen acceso a las tile_static variables, no hay ningún orden garantizado de ejecución de subprocesos en el icono. En el ejemplo siguiente se muestra cómo usar tile_static variables y el tile_barrier::wait método para calcular el valor medio de cada icono. Estas son las claves para comprender el ejemplo:

  1. RawData se almacena en una matriz 8x8.

  2. El tamaño del icono es 2x2. Esto crea una cuadrícula de mosaicos de 4x4, y los promedios se pueden almacenar mediante un objeto array en una matriz de 4x4. Solo hay un número limitado de tipos que puede capturar por referencia en una función restringida de AMP. La array clase es una de ellas.

  3. El tamaño de la matriz y el tamaño de la muestra se definen mediante instrucciones , #define ya que los parámetros de tipo en array, array_view, extenty tiled_index deben ser valores constantes. También puede usar declaraciones const int static. Como ventaja adicional, es trivial cambiar el tamaño de la muestra para calcular el promedio sobre mosaicos de 4 x 4.

  4. Se declara una tile_static matriz de 2x2 de valores flotantes para cada baldosa. Aunque la declaración está en la ruta de código de cada subproceso, solo se crea una matriz para cada tesela de la matriz.

  5. Hay una línea de código para copiar los valores de cada icono en la tile_static matriz. Para cada subproceso, después de copiar el valor en la matriz, la ejecución en el subproceso se detiene debido a la llamada a tile_barrier::wait.

  6. Cuando todos los subprocesos de un icono han alcanzado la barrera, se puede calcular el promedio. Dado que el código se ejecuta para cada subproceso, hay una if instrucción para calcular solo el promedio en un subproceso. El promedio se almacena en la variable averages. La barrera es esencialmente la estructura que controla los cálculos por mosaico, al igual que se podría usar un for bucle.

  7. Los datos de la averages variable, ya que es un array objeto, deben copiarse de nuevo en el host. En este ejemplo se usa el operador de conversión de vectores.

  8. En el ejemplo completo, puede cambiar SAMPLESIZE a 4 y el código se ejecuta correctamente sin ningún otro cambio.

#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();
}

Condiciones de carrera

Puede ser tentador crear una tile_static variable denominada total e incrementar esa variable para cada subproceso, como esta:

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

El primer problema con este enfoque es que tile_static las variables no pueden tener inicializadores. El segundo problema es que hay una condición de carrera en la asignación a total, porque todos los subprocesos del icono tienen acceso a la variable en ningún orden determinado. Puede programar un algoritmo para solo permitir que un hilo acceda al total en cada barrera, como se muestra a continuación. Sin embargo, esta solución no es extensible.

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

Barreras de memoria

Hay dos tipos de accesos de memoria que deben sincronizarse: la memoria global y la memoria tile_static. Un concurrency::array objeto asigna solo memoria global. Un concurrency::array_view puede hacer referencia a memoria global, a tile_static memoria, o a ambas, en función de cómo se construyó. Hay dos tipos de memoria que se deben sincronizar:

  • memoria global

  • tile_static

Una barrera de memoria garantiza que los accesos a memoria están disponibles para otros subprocesos del icono de subprocesos y que los accesos a memoria se ejecutan según el orden del programa. Para asegurar esto, los compiladores y procesadores no reordenan las lecturas y escrituras a través de la barrera de memoria. En C++ AMP, una barrera de memoria se crea mediante una llamada a uno de estos métodos:

Llamar a la barrera específica que necesita puede mejorar el rendimiento de la aplicación. El tipo de barrera afecta a cómo el compilador y el hardware reordenan las sentencias. Por ejemplo, si usa una barrera de memoria global, solo se aplica a los accesos a memoria global y, por lo tanto, el compilador y el hardware pueden reordenar las lecturas y escrituras en tile_static variables en los dos lados de la barrera.

En el ejemplo siguiente, la barrera sincroniza las escrituras en tileValues, una variable tile_static. En este ejemplo, tile_barrier::wait_with_tile_static_memory_fence se llama a en lugar 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);
    }
});

Consulte también

C++ AMP (Paralelismo masivo acelerado de C++)
tile_static Palabra clave