Tutorial: Multiplicación de matrices
Este tutorial muestra, paso a paso, cómo utilizar el AMP de C++ para acelerar la ejecución de la multiplicación de matrices.Se presentan dos algoritmos, uno sin mosaico y otro con mosaico.
Requisitos previos
Antes de comenzar:
Lea Usar mosaicos.
Asegúrese de que Windows 7, Windows 8, Windows Server 2008 R2 o Windows Server 2012 está instalado en su ordenador.
Para crear el proyecto
En la barra del menu de Visual Studio, elija Archivo, Nuevo, Proyecto.
Debajo de Instalado en el panel de las plantillas, seleccione Visual C++.
Seleccione Proyecto vacío, escriba MatrixMultiply en el cuadro de Nombre y después elija el botón de Aceptar .
Elija el botón de Siguiente .
En el Explorador de soluciones, abra el acceso directo para Archivos de origeny, a continuación, elija agregar, Nuevo elemento.
En el cuadro de diálogo de Agregar nuevo elemento, elija Archivo C++ (.cpp), escriba MatrixMultiply.cpp en el cuadro de Nombre y después elija el botón de agregar .
Multiplicación sin mosaico
En esta sección, considere la multiplicación de dos matrices, A y B, que se definen a continuación:
A es una matriz de 3 por 2 y B es una matriz 2 por 3.El producto de multiplicar A por B es la siguiente matriz de 3 por 3.El producto se calcula multiplicando las filas de A por las columnas de B elemento por elemento.
Para multiplicar sin utilizar el AMP de C++
Abra el archivo MatrixMultiply.cpp y utilice el siguiente código para reemplazar el código existente.
#include <iostream> void MultiplyWithOutAMP() { int aMatrix[3][2] = {{1, 4}, {2, 5}, {3, 6}}; int bMatrix[2][3] = {{7, 8, 9}, {10, 11, 12}}; int product[3][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { // Multiply the row of A by the column of B to get the row, column of product. for (int inner = 0; inner < 2; inner++) { product[row][col] += aMatrix[row][inner] * bMatrix[inner][col]; } std::cout << product[row][col] << " "; } std::cout << "\n"; } } void main() { MultiplyWithOutAMP(); getchar(); }
El algoritmo es una implementación directa de la definición de multiplicación de la matriz.No utiliza ningún algoritmo paralelo o de proceso para reducir los tiempos de cálculo
En la barra de menú, elija Archivo, Guardar Todos.
Seleccione el atajo del teclado F5 para iniciar la depuración y verifique que la salida es correcta.
Elija Enter para salir de la aplicación.
Para multiplicar mediante AMP de C++
En MatrixMultiply.cpp, agregue el siguiente código antes del método main.
void MultiplyWithAMP() { int aMatrix[] = { 1, 4, 2, 5, 3, 6 }; int bMatrix[] = { 7, 8, 9, 10, 11, 12 }; int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }; array_view<int, 2> a(3, 2, aMatrix); array_view<int, 2> b(2, 3, bMatrix); array_view<int, 2> product(3, 3, productMatrix); parallel_for_each( product.extent, [=](index<2> idx) restrict(amp) { int row = idx[0]; int col = idx[1]; for (int inner = 0; inner < 2; inner++) { product[idx] += a(row, inner) * b(inner, col); } } ); product.synchronize(); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { //std::cout << productMatrix[row*3 + col] << " "; std::cout << product(row, col) << " "; } std::cout << "\n"; } }
El código de AMP es similar al código de no-AMP.La llamada a parallel_for_each inicia un subproceso para cada elemento de product.extent y reemplaza los bucles de for por fila y columna.El valor de la celda en la fila y la columna se encuentra disponible en idx.Puede tener acceso a los elementos de un objeto array_view, bien utilizando el operador [] y una variable de índice, o el operador de () y las variables de fila y columna.Este ejemplo da una demostración de ambos métodos.El método array_view::synchronize copia los valores de la variable product en la variable productMatrix.
Agregue las siguientes declaraciones include y using en la parte superior de MatrixMultiply.cpp.
#include <amp.h> using namespace concurrency;
Modifique el método main para llamar el método MultiplyWithAMP.
void main() { MultiplyWithOutAMP(); MultiplyWithAMP(); getchar(); }
Seleccione el atajo de teclado Ctrl+F5 para iniciar la depuración y verifique que la salida es correcta.
Elija la barra espaciadora para salir de la aplicación.
Multiplicación con mosaico
El mosaico es una técnica en la que se crean particiones de datos en subconjuntos de igual tamaño, que se conocen como tiles.Tres cosas cambian cuando se utiliza el mosaico.
Puede crear variables tile_static.El acceso a los datos en el espacio de tile_static puede ser mucho más rápido que el acceso a los datos del espacio global.Se crea una instancia de la variable tile_static para cada mosaico y todos los subprocesos del mosaico tienen acceso a la variable.La ventaja fundamental del mosaico es que mejora el rendimiento debido al acceso tile_static.
Se puede llamar al método tile_barrier::wait para detener todos los subprocesos en un mosaico dentro una línea de código especifica.No se puede garantizar el orden en que los subprocesos se van a ejecutar, sólo se garantiza que todos los subprocesos que estén en un mosaico se detendrán al llamar a tile_barrier::wait, antes de continuar la ejecución.
Se tiene acceso al índice del subproceso con respecto a la totalidad del objeto array_view y al índice con respecto al mosaico.Al utilizar el índice local, se facilita que el código sea más fácil de leer y depurar.
Para aprovechar el mosaico en la multiplicación de la matriz, el algoritmo debe dividir la matriz en mosaicos y luego copiar los datos de los mosaicos en las variables tile_static para un acceso más rapido.En este ejemplo, la matriz es dividida en submatrices de igual tamaño.El producto se obtiene multiplicando los submatrices.Las dos matrices y su producto en este ejemplo son:
Las matrices se dividen en cuatro matrices de 2 por 2, tal como se define a continuación:
El producto de A y B ahora se puede escribir y calcular tal y como se muestra a continuación:
Dado que las matrices a a través de h son matrices de 2x2, todos los productos y sumas de ellos también son matrices de 2x2.También se deduce que A*B es una matriz de 4x4, tal y como se esperaba.Para comprobar rápidamente el algoritmo, calcule el valor del elemento de la primera fila, primera columna en el producto.En el ejemplo, esto sería el valor del elemento de la primera fila y la primera columna de ae + bg.Solo tiene que calcular la primera columna, la primera fila de ae y bg para cada término.El valor para ae es 1*1 + 2*5 = 11.El valor de bg es 3*1 + 4*5 = 23.El valor total es 11 + 23 = 34, que es correcto.
Para implementar el algoritmo, el código:
Usa un objeto de tiled_extent en lugar de un objeto de extent en la llamada de parallel_for_each.
Usa un objeto de tiled_index en vez de un objeto de index en la llamada de parallel_for_each.
Crea variables de tile_static para contener las submatrices.
Utiliza el método tile_barrier::wait para detener los subprocesos para el cálculo de los productos de las submatrices.
Para multiplicar utilizando AMP y el mosaico.
En MatrixMultiply.cpp, agregue el siguiente código antes del método main .
void MultiplyWithTiling() { // The tile size is 2. static const int TS = 2; // The raw data. int aMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 }; int bMatrix[] = { 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8 }; int productMatrix[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // Create the array_view objects. array_view<int, 2> a(4, 4, aMatrix); array_view<int, 2> b(4, 4, bMatrix); array_view<int, 2> product(4, 4, productMatrix); // Call parallel_for_each by using 2x2 tiles. parallel_for_each(product.extent.tile< TS, TS >(), [=] (tiled_index< TS, TS> t_idx) restrict(amp) { // Get the location of the thread relative to the tile (row, col) and the entire array_view (rowGlobal, colGlobal). int row = t_idx.local[0]; int col = t_idx.local[1]; int rowGlobal = t_idx.global[0]; int colGlobal = t_idx.global[1]; int sum = 0; // Given a 4x4 matrix and a 2x2 tile size, this loop executes twice for each thread. // For the first tile and the first loop, it copies a into locA and e into locB. // For the first tile and the second loop, it copies b into locA and g into locB. for (int i = 0; i < 4; i += TS) { tile_static int locA[TS][TS]; tile_static int locB[TS][TS]; locA[row][col] = a(rowGlobal, col + i); locB[row][col] = b(row + i, colGlobal); // The threads in the tile all wait here until locA and locB are filled. t_idx.barrier.wait(); // Return the product for the thread. The sum is retained across // both iterations of the loop, in effect adding the two products // together, for example, a*e. for (int k = 0; k < TS; k++) { sum += locA[row][k] * locB[k][col]; } // All threads must wait until the sums are calculated. If any threads // moved ahead, the values in locA and locB would change. t_idx.barrier.wait(); // Now go on to the next iteration of the loop. } // After both iterations of the loop, copy the sum to the product variable by using the global location. product[t_idx.global] = sum; }); // Copy the contents of product back to the productMatrix variable. product.synchronize(); for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { // The results are available from both the product and productMatrix variables. //std::cout << productMatrix[row*3 + col] << " "; std::cout << product(row, col) << " "; } std::cout << "\n"; } }
Este ejemplo es significativamente diferente respecto al ejemplo sin mosaico.El código usa estos pasos conceptuales:
Copie los elementos del mosaico [0,0] de a en locA.Copie los elementos del mosaico [0,0] de b en locB.Observe que product está en el mosaico, no a y b.Por lo tanto, utilice índices globales para tener acceso a a, b y product.La llamada a tile_barrier::wait es fundamental.Esto detiene todos los subprocesos del mosaico hasta que se rellenen locA y locB .
Multiplique locA y locB y coloque los resultados en product.
Copie los elementos del mosaico [0,1] de a en locA.Copie los elementos del mosaico [1,0] de b en locB.
Multiplique locA y locB y añadalos a los resultados que ya están en product.
La multiplicación del mosaico [0,0] se ha completado.
Repita para los otros cuatro mosaicos.No existe indexación específica para los mosaicos, por lo tanto, los subprocesos se pueden ejecutar en cualquier orden.A medida que cada subproceso se ejecuta, se crean las variables de tile_static para cada mosaico y la llamada a tile_barrier::wait controla el flujo del programa.
Al examinar el algoritmo detenidamente, se observa que cada submatriz se carga en memoria tile_static dos veces.Esta transferencia de datos lleva tiempo.Sin embargo, una vez que los datos están en la memoria tile_static, el acceso a los datos es mucho más rápido.Debido a que el cálculo de los productos requiere el acceso repetido a los valores de las submatrices, hay un aumento del rendimiento.Para cada algoritmo, se requiere praćtica para poder encontrar el algoritmo óptimo y el tamaño del mosaico.
En el no-AMP y en los ejemplos de no-mosaico, a cada elemento de A y B se accede cuatro veces desde la memoria global para calcular el producto.En el ejemplo del mosaico, a cada elemento se accede dos veces a partir de la memoria global y cuatro veces desde la memoria de tile_static.Esto no supone una ganancia significativa de rendimiento.Sin embargo, si A y B fueran matrices de 1024x1024 y el tamaño del mosaico fuese 16, habría una ganancia de rendimiento significativa.En ese caso, cada elemento puede ser copiado en la memoria tile_static sólo 16 veces y acceder a él desde la memoria tile_static 1024 veces.
Modifique el método main para llamar al método de MultiplyWithTiling, como se muestra.
void main() { MultiplyWithOutAMP(); MultiplyWithAMP(); MultiplyWithTiling(); getchar(); }
Seleccione el atajo de teclado Ctrl+F5 para iniciar la depuración y verifique que la salida es correcta.
Elija la barra espaciadora para salir de la aplicación.
Vea también
Tareas
Tutorial: Depurar una aplicación de C++ AMP