Compartir a través de


Usar mosaicos

Puede usar mosaicos para maximizar la aceleración de la aplicación. Para ello, se dividen los subprocesos en subconjuntos rectangulares iguales o mosaicos. Si usa un tamaño de mosaico y un algoritmo de mosaico adecuados, puede obtener una aceleración todavía mayor del código de C++ AMP. Los componentes básicos de los mosaicos son los siguientes:

  • Variables tile_static. La ventaja fundamental de los mosaicos es que mejoran el rendimiento debido al acceso tile_static. El acceso a los datos de la memoria tile_static puede ser considerablemente más rápido que el acceso a los datos del espacio global (objetos array o array_view). 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 en mosaico típico, los datos se copian una vez en la memoria tile_static desde la memoria global y, luego, se accede a ellos muchas veces desde la memoria tile_static.

  • Método tile_barrier::wait. Una llamada a tile_barrier::wait suspende la ejecución del subproceso actual hasta que todos los subprocesos del mismo mosaico alcanzan 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 mosaico se ejecutará tras la llamada a tile_barrier::wait hasta que todos los subprocesos la hayan alcanzado. Esto significa que, con el método tile_barrier::wait, puede realizar tareas por mosaico, en lugar de por subproceso. Un algoritmo de mosaico típico tiene código para inicializar la memoria tile_static de todo el mosaico seguido de una llamada a tile_barrier::wait. El código que aparece después de tile_barrier::wait contiene cálculos que requieren acceso a todos los valores tile_static.

  • Indexación local y global. Tiene acceso al índice del subproceso que concierne a la totalidad del objeto array_view o array y al índice que concierne al mosaico. Al usar el índice local, se facilita la lectura y la depuración del código. Normalmente, se usa la indexación local para acceder a variables tile_static y la indexación global para acceder a variables array y array_view.

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

Para aprovechar las ventajas del mosaico, el algoritmo debe dividir el dominio de proceso en mosaicos y, luego, copiar los datos de los mosaicos en variables tile_static para un acceso más rápido.

Ejemplo de índices globales, de mosaico 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 datos de 8 × 9 que se organiza en mosaicos de 2 × 3.

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

En el ejemplo siguiente se muestran los índices globales, de mosaico y locales de esta matriz en mosaico. Se crea un objeto array_view mediante elementos de tipo Description. En Description se incluyen 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 mosaico y locales de cada elemento. En la salida se muestran los valores de 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 objeto array_view y la llamada a parallel_for_each.

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

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

  3. Se llama al método parallel_for_each mediante un objeto tiled_index<2,3> (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 mosaico en el que se encuentra el subproceso (la propiedad tiled_index::tile) y la ubicación del subproceso dentro del mosaico (la propiedad tiled_index::local).

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

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

  1. El objeto rawData se almacena en una matriz de 8 × 8.

  2. El tamaño del mosaico es de 2 × 2. Esto crea una cuadrícula de 4 × 4 de mosaicos y los promedios se pueden almacenar en una matriz de 4 × 4 mediante un objeto array. Solo hay un número limitado de tipos que se pueden capturar por referencia en una función con restricción amp. La clase array es uno de ellos.

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

  4. Se declara una matriz tile_static de 2 × 2 de valores float para cada mosaico. Aunque la declaración está en la ruta de acceso al código para cada subproceso, solo se crea una matriz para cada mosaico de la matriz.

  5. Hay una línea de código para copiar los valores de cada mosaico en la matriz tile_static. 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 mosaico han alcanzado la barrera, se puede calcular el promedio. Dado que el código se ejecuta para cada subproceso, hay una instrucción if solo para calcular el promedio en un subproceso. El promedio se almacena en la variable averages. La barrera es básicamente la construcción que controla los cálculos por mosaico, como si se usara un bucle for.

  7. Como se trata de un objeto array, los datos de la variable averages deben volver a copiarse en el host. En este ejemplo se usa el operador de conversión vectorial.

  8. En el ejemplo completo, puede cambiar SAMPLESIZE a 4 y el código se ejecutará 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 resultar tentador crear una variable tile_static denominada total e incrementar esa variable para cada subproceso, como se muestra a continuación:

// 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 de este enfoque es que las variables tile_static no pueden tener inicializadores. El segundo problema es que hay una condición de carrera en la asignación a total, dado que todos los subprocesos del mosaico tienen acceso a la variable sin ningún orden determinado. Podría programar un algoritmo para permitir que un subproceso acceda al total en cada barrera, como se muestra a continuación, pero 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 se deben sincronizar: el acceso a la memoria global y el acceso a la memoria tile_static. Un objeto concurrency::array asigna solo memoria global. Un objeto concurrency::array_view puede hacer referencia a la memoria global, la memoria tile_static o ambas, en función de cómo se construyó. Hay dos tipos de memoria que se deben sincronizar:

  • La memoria global

  • tile_static

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

Si llama a la barrera específica que necesita, puede mejorar el rendimiento de la aplicación. El tipo de barrera afecta a la forma en que el compilador y el hardware reordenan las instrucciones. Por ejemplo, si usa una barrera de memoria global, se aplica únicamente a los accesos a la memoria global y, por lo tanto, el compilador y el hardware podrían reordenar las lecturas y escrituras en variables tile_static a ambos lados de la barrera.

En el ejemplo siguiente, la barrera sincroniza las escrituras en tileValues, una variable tile_static. En este ejemplo, se llama a tile_barrier::wait_with_tile_static_memory_fence, 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 (C++ Accelerated Massive Parallelism)
tile_static (Palabra clave)