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 accesotile_static
. El acceso a los datos de la memoriatile_static
puede ser considerablemente más rápido que el acceso a los datos del espacio global (objetosarray
oarray_view
). Se crea una instancia de la variabletile_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 memoriatile_static
desde la memoria global y, luego, se accede a ellos muchas veces desde la memoriatile_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 atile_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 atile_barrier::wait
hasta que todos los subprocesos la hayan alcanzado. Esto significa que, con el métodotile_barrier::wait
, puede realizar tareas por mosaico, en lugar de por subproceso. Un algoritmo de mosaico típico tiene código para inicializar la memoriatile_static
de todo el mosaico seguido de una llamada atile_barrier::wait
. El código que aparece después detile_barrier::wait
contiene cálculos que requieren acceso a todos los valorestile_static
.Indexación local y global. Tiene acceso al índice del subproceso que concierne a la totalidad del objeto
array_view
oarray
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 variablestile_static
y la indexación global para acceder a variablesarray
yarray_view
.Clase tiled_extent y clase tiled_index. Se usa un objeto
tiled_extent
en lugar de un objetoextent
en la llamada aparallel_for_each
. Se usa un objetotiled_index
en lugar de un objetoindex
en la llamada aparallel_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.
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
.
El vector de estructuras
Description
se copia en un objetoarray_view
de 8 × 9.Se llama al método
parallel_for_each
con un objetotiled_extent
como dominio de proceso. El objetotiled_extent
se crea mediante una llamada al métodoextent::tile()
de la variabledescriptions
. Los parámetros de tipo de la llamada aextent::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.Se llama al método
parallel_for_each
mediante un objetotiled_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>()
).Cuando se ejecuta cada subproceso, el índice
t_idx
devuelve información sobre el mosaico en el que se encuentra el subproceso (la propiedadtiled_index::tile
) y la ubicación del subproceso dentro del mosaico (la propiedadtiled_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:
El objeto rawData se almacena en una matriz de 8 × 8.
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 clasearray
es uno de ellos.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 paraarray
,array_view
,extent
ytiled_index
deben ser valores constantes. También se pueden usar declaracionesconst 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.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.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 atile_barrier::wait
.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 buclefor
.Como se trata de un objeto
array
, los datos de la variableaverages
deben volver a copiarse en el host. En este ejemplo se usa el operador de conversión vectorial.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:
Método tile_barrier::wait: crea una barrera en torno a la memoria global y
tile_static
.Método tile_barrier::wait_with_all_memory_fence: crea una barrera en torno a la memoria global y
tile_static
.Método tile_barrier::wait_with_global_memory_fence: crea una barrera solo en torno a la memoria global.
Método tile_barrier::wait_with_tile_static_memory_fence: crea una barrera solo en torno a la memoria
tile_static
.
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)