Información general sobre C++ AMP
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.
C++ Accelerated Massive Parallelism (C++ AMP) acelera la ejecución de código de C++ al aprovechar las ventajas del hardware de datos en paralelo, como una unidad de procesamiento gráfico (GPU) en una tarjeta gráfica discreta. Mediante el uso de C++ AMP, puede codificar algoritmos de datos multidimensionales para que la ejecución se pueda acelerar mediante el paralelismo en hardware heterogéneo. El modelo de programación de C++ AMP incluye matrices multidimensionales, indexación, transferencia de memoria, disposición en mosaico y una biblioteca de funciones matemáticas. Puede usar extensiones de lenguaje de C++ AMP para controlar cómo se mueven los datos de la CPU a la GPU y viceversa, lo que le permite mejorar el rendimiento.
Requisitos del sistema
Windows 7 o posterior
Windows Server 2008 R2 a Visual Studio 2019.
Hardware DirectX 11 con un nivel de características 11.0 o superior
Para la depuración en el emulador de software, se requiere Windows 8 o Windows Server 2012. Para depurar en el hardware, debe instalar los controladores de su tarjeta gráfica. Para obtener más información, consulte Depurar código GPU.
Nota: AMP no se admite actualmente en ARM64.
Introducción
En los dos ejemplos siguientes se muestran los componentes principales de C++ AMP. Supongamos que quiere agregar los elementos correspondientes de dos matrices unidimensionales. Por ejemplo, quiere sumar {1, 2, 3, 4, 5}
y {6, 7, 8, 9, 10}
para obtener {7, 9, 11, 13, 15}
. Si no usa C++ AMP, escribiría el código siguiente para sumar los números y mostrar los resultados.
#include <iostream>
void StandardMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5];
for (int idx = 0; idx < 5; idx++)
{
sumCPP[idx] = aCPP[idx] + bCPP[idx];
}
for (int idx = 0; idx < 5; idx++)
{
std::cout << sumCPP[idx] << "\n";
}
}
Las partes importantes del código son las siguientes:
Datos: constan de tres matrices. Todos tienen la misma clasificación (uno) y longitud (cinco).
Iteración: el primer bucle
for
proporciona un mecanismo para iterar por los elementos de las matrices. El código que quiere ejecutar para calcular las sumas se encuentra en el primer bloquefor
.Índice: la variable
idx
accede a los elementos individuales de las matrices.
Con C++ AMP, puede escribir el código siguiente.
#include <amp.h>
#include <iostream>
using namespace concurrency;
const int size = 5;
void CppAmpMethod() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[size];
// Create C++ AMP objects.
array_view<const int, 1> a(size, aCPP);
array_view<const int, 1> b(size, bCPP);
array_view<int, 1> sum(size, sumCPP);
sum.discard_data();
parallel_for_each(
// Define the compute domain, which is the set of threads that are created.
sum.extent,
// Define the code to run on each thread on the accelerator.
[=](index<1> idx) restrict(amp) {
sum[idx] = a[idx] + b[idx];
}
);
// Print the results. The expected output is "7, 9, 11, 13, 15".
for (int i = 0; i < size; i++) {
std::cout << sum[i] << "\n";
}
}
Están presentes los mismos elementos básicos, pero se usan construcciones de C++ AMP:
Datos: se usan matrices de C++ para construir tres objetos array_view de C++ AMP. Se proporcionan cuatro valores para construir un objeto
array_view
: los valores de datos, la clasificación, el tipo de elemento y la longitud del objetoarray_view
en cada dimensión. La clasificación y el tipo se pasan como parámetros de tipo. Los datos y la longitud se pasan como parámetros de constructor. En este ejemplo, la matriz de C++ que se pasa al constructor es unidimensional. La clasificación y la longitud se usan para construir la forma rectangular de los datos en el objetoarray_view
y los valores de datos se usan para rellenar la matriz. La biblioteca en tiempo de ejecución también incluye la clase array, que tiene una interfaz similar a la clasearray_view
y se describe más adelante en este artículo.Iteración: la función parallel_for_each (C++ AMP) proporciona un mecanismo para iterar por los elementos de datos o el dominio de proceso. En este ejemplo, el dominio de proceso se especifica mediante
sum.extent
. El código que quiere ejecutar se encuentra en una expresión lambda, o función kernel.restrict(amp)
indica que solo se usa el subconjunto del lenguaje C++ que C++ AMP pueda acelerar.Índice: la variable de clase index,
idx
, se declara con una clasificación de uno para que coincida con la clasificación del objetoarray_view
. Mediante el uso del índice, se puede acceder a los elementos individuales de los objetosarray_view
.
Modelado e indexación de datos: índice y extensión
Debe definir los valores de los datos y declarar la forma de los datos para poder ejecutar el código de kernel. Todos los datos se definen como una matriz (rectangular), y la matriz puede definirse de modo que tenga cualquier clasificación (número de dimensiones). Los datos pueden tener cualquier tamaño en cualquiera de las dimensiones.
index (Clase)
La clase index especifica una ubicación en el objeto array
o array_view
. Para ello, encapsula el desplazamiento desde el origen de cada dimensión a un objeto. Cuando se obtiene acceso a una ubicación de la matriz, se pasa un objeto index
al operador de indexación, []
, en lugar de una lista de índices enteros. Puede acceder a los elementos de cada dimensión mediante el operador array::operator() o el operador array_view::operator().
En el ejemplo siguiente se crea un índice unidimensional que especifica el tercer elemento de un objeto array_view
unidimensional. El índice se usa para imprimir el tercer elemento del objeto array_view
. El resultado es 3.
int aCPP[] = {1, 2, 3, 4, 5};
array_view<int, 1> a(5, aCPP);
index<1> idx(2);
std::cout << a[idx] << "\n";
// Output: 3
En el ejemplo siguiente se crea un índice bidimensional que especifica el elemento, donde la fila es igual a 1 y la columna es igual a 2 en un objeto array_view
bidimensional. El primer parámetro del constructor index
es el componente de fila y el segundo es el componente de columna. El resultado es 6.
int aCPP[] = {1, 2, 3, 4, 5, 6};
array_view<int, 2> a(2, 3, aCPP);
index<2> idx(1, 2);
std::cout <<a[idx] << "\n";
// Output: 6
En el ejemplo siguiente se crea un índice tridimensional que especifica el elemento, donde la profundidad es igual a 0, la fila es igual a 1 y la columna es igual a 3 en un objeto array_view
tridimensional. Observe que el primer parámetro es el componente de profundidad, el segundo es el componente de fila y el tercero es el componente de columna. El resultado es 8.
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
array_view<int, 3> a(2, 3, 4, aCPP);
// Specifies the element at 3, 1, 0.
index<3> idx(0, 1, 3);
std::cout << a[idx] << "\n";
// Output: 8
extent (Clase)
La clase extent especifica la longitud de los datos en cada dimensión del objeto array
o array_view
. Puede crear una extensión y usarla para crear un objeto array
o array_view
. También puede recuperar la extensión de un objeto array
o array_view
existente. En el ejemplo siguiente se imprime la longitud de la extensión en cada dimensión de un objeto array_view
.
int aCPP[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
// There are 3 rows and 4 columns, and the depth is two.
array_view<int, 3> a(2, 3, 4, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
std::cout << "Length in most significant dimension is " << a.extent[0] << "\n";
En el ejemplo siguiente se crea un objeto array_view
que tiene las mismas dimensiones que el objeto del ejemplo anterior, pero en este caso se usa un objeto extent
, en lugar de usar parámetros explícitos en el constructor array_view
.
int aCPP[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24};
extent<3> e(2, 3, 4);
array_view<int, 3> a(e, aCPP);
std::cout << "The number of columns is " << a.extent[2] << "\n";
std::cout << "The number of rows is " << a.extent[1] << "\n";
std::cout << "The depth is " << a.extent[0] << "\n";
Movimiento de datos al acelerador: array y array_view
En la biblioteca en tiempo de ejecución se definen dos contenedores de datos que se usan para mover datos al acelerador. Se trata de la clase array y la clase array_view. La clase array
es una clase contenedora que crea una copia en profundidad de los datos cuando se construye el objeto. La clase array_view
es una clase encapsuladora que copia los datos cuando la función kernel accede a los datos. Cuando se necesitan los datos en el dispositivo de origen, los datos se copian de nuevo.
array (Clase)
Cuando se construye un objeto array
, se crea una copia en profundidad de los datos en el acelerador si se usa un constructor que incluye un puntero al conjunto de datos. La función kernel modifica la copia en el acelerador. Cuando finaliza la ejecución de la función kernel, los datos deben volver a copiarse en la estructura de datos de origen. En el ejemplo siguiente se multiplica por 10 cada elemento de un vector. Una vez que finaliza la función kernel, se usa vector conversion operator
para volver a copiar los datos en el objeto vectorial.
std::vector<int> data(5);
for (int count = 0; count <5; count++)
{
data[count] = count;
}
array<int, 1> a(5, data.begin(), data.end());
parallel_for_each(
a.extent,
[=, &a](index<1> idx) restrict(amp) {
a[idx] = a[idx]* 10;
});
data = a;
for (int i = 0; i < 5; i++)
{
std::cout << data[i] << "\n";
}
array_view (Clase)
array_view
tiene prácticamente los mismos miembros que la clase array
, pero el comportamiento subyacente no es el mismo. Los datos que se pasan al constructor array_view
no se replican en la GPU, ya que se trata de un constructor array
. En su lugar, los datos se copian en el acelerador cuando se ejecuta la función kernel. Por lo tanto, si crea dos objetos array_view
que usan los mismos datos, ambos objetos array_view
hacen referencia al mismo espacio de memoria. Al hacerlo, debe sincronizar cualquier acceso multiproceso. La principal ventaja de usar la clase array_view
es que los datos se mueven solo si es necesario.
Comparación de array y array_view
En la tabla siguiente se resumen las similitudes y las diferencias entre las clases array
y array_view
.
Descripción | array (clase) | array_view (clase) |
---|---|---|
Cuándo se determina la clasificación | En tiempo de compilación. | En tiempo de compilación. |
Cuándo se determina la extensión | En tiempo de ejecución. | En tiempo de ejecución. |
Forma | Rectangular. | Rectangular. |
Almacenamiento de datos | Es un contenedor de datos. | Es un encapsulador de datos. |
Copiar | Copia explícita y en profundidad en la definición. | Copia implícita cuando la función kernel accede a ella. |
Recuperación de datos | Mediante la copia de los datos de la matriz de nuevo en un objeto en el subproceso de CPU. | Mediante el acceso directo al objeto array_view o una llamada al método array_view::synchronize para seguir accediendo a los datos del contenedor original. |
Memoria compartida con array y array_view
La memoria compartida es aquella a la que pueden acceder tanto la CPU como el acelerador. El uso de memoria compartida elimina o reduce considerablemente la sobrecarga de copiar datos entre la CPU y el acelerador. Aunque la memoria se comparte, la CPU y el acelerador no pueden acceder a ella simultáneamente; si lo hacen, se produce un comportamiento indefinido.
Se pueden usar objetos array
para especificar un control pormenorizado sobre el uso de memoria compartida si el acelerador asociado lo admite. Si un acelerador admite memoria compartida viene determinado por la propiedad supports_cpu_shared_memory del acelerador, que devuelve true
cuando se admite la memoria compartida. Si se admite la memoria compartida, la propiedad default_cpu_access_type
determina la enumeración access_type predeterminada para las asignaciones de memoria en el acelerador. De forma predeterminada, los objetos array
y array_view
toman el mismo access_type
que el objeto accelerator
asociado principal.
Al establecer la propiedad array::cpu_access_type Data Member de un elemento array
explícitamente, puede ejercer un control específico sobre cómo se usa la memoria compartida, de modo que pueda optimizar la aplicación para las características de rendimiento del hardware, en función de los patrones de acceso a memoria de sus kernels de cálculo. Un array_view
refleja lo mismo cpu_access_type
que el array
asociado a; o, si el array_view se construye sin un origen de datos, access_type
refleja el entorno que primero hace que asigne almacenamiento. Es decir, si el host (CPU) accede por primera vez a él, se comporta como si se creara a través de un origen de datos de CPU y comparta el access_type
de asociado accelerator_view
por captura; sin embargo, si se accede por primera vez a él por un accelerator_view
, se comporta como si se creara sobre un array
creado en ese accelerator_view
y comparte el array
access_type
objeto .
En el ejemplo de código siguiente se muestra cómo determinar si el acelerador predeterminado admite la memoria compartida y, luego, se crean varias matrices que tienen configuraciones de cpu_access_type diferentes.
#include <amp.h>
#include <iostream>
using namespace Concurrency;
int main()
{
accelerator acc = accelerator(accelerator::default_accelerator);
// Early out if the default accelerator doesn't support shared memory.
if (!acc.supports_cpu_shared_memory)
{
std::cout << "The default accelerator does not support shared memory" << std::endl;
return 1;
}
// Override the default CPU access type.
acc.default_cpu_access_type = access_type_read_write
// Create an accelerator_view from the default accelerator. The
// accelerator_view inherits its default_cpu_access_type from acc.
accelerator_view acc_v = acc.default_view;
// Create an extent object to size the arrays.
extent<1> ex(10);
// Input array that can be written on the CPU.
array<int, 1> arr_w(ex, acc_v, access_type_write);
// Output array that can be read on the CPU.
array<int, 1> arr_r(ex, acc_v, access_type_read);
// Read-write array that can be both written to and read from on the CPU.
array<int, 1> arr_rw(ex, acc_v, access_type_read_write);
}
Ejecución de código con datos: parallel_for_each
La función parallel_for_each define el código que quiere ejecutar en el acelerador con los datos del objeto array
o array_view
. Observe el código siguiente de la introducción de este tema.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddArrays() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp)
{
sum[idx] = a[idx] + b[idx];
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
El método parallel_for_each
toma dos argumentos, un dominio de proceso y una expresión lambda.
El dominio de proceso es un objeto extent
o un objeto tiled_extent
que define el conjunto de subprocesos que se van a crear para la ejecución en paralelo. Se genera un subproceso para cada elemento del dominio de proceso. En este caso, el objeto extent
es unidimensional y tiene cinco elementos. Por lo tanto, se inician cinco subprocesos.
La expresión lambda define el código que se va a ejecutar en cada subproceso. La cláusula de captura, [=]
, especifica que el cuerpo de la expresión lambda accede a todas las variables capturadas por valor, que en este caso son a
, b
y sum
. En este ejemplo, la lista de parámetros crea una variable index
unidimensional denominada idx
. El valor de idx[0]
es 0 en el primer subproceso y aumenta uno en cada subproceso posterior. restrict(amp)
indica que solo se usa el subconjunto del lenguaje C++ que C++ AMP pueda acelerar. Las limitaciones de las funciones que tienen el modificador restrict se describen en restrict (C++ AMP). Para obtener más información, consulte Sintaxis de la expresión lambda.
La expresión lambda puede incluir el código que se va a ejecutar o puede llamar a una función kernel independiente. La función kernel debe incluir el modificador restrict(amp)
. El ejemplo siguiente es equivalente al ejemplo anterior, pero llama a una función kernel independiente.
#include <amp.h>
#include <iostream>
using namespace concurrency;
void AddElements(
index<1> idx,
array_view<int, 1> sum,
array_view<int, 1> a,
array_view<int, 1> b) restrict(amp) {
sum[idx] = a[idx] + b[idx];
}
void AddArraysWithFunction() {
int aCPP[] = {1, 2, 3, 4, 5};
int bCPP[] = {6, 7, 8, 9, 10};
int sumCPP[5] = {0, 0, 0, 0, 0};
array_view<int, 1> a(5, aCPP);
array_view<int, 1> b(5, bCPP);
array_view<int, 1> sum(5, sumCPP);
parallel_for_each(
sum.extent,
[=](index<1> idx) restrict(amp) {
AddElements(idx, sum, a, b);
}
);
for (int i = 0; i < 5; i++) {
std::cout << sum[i] << "\n";
}
}
Aceleración del código: mosaicos y barreras
Puede obtener una aceleración adicional mediante el uso de mosaicos. Para ello, se dividen los subprocesos en subconjuntos rectangulares iguales o mosaicos. Determine el tamaño de mosaico adecuado en función del conjunto de datos y del algoritmo que está codificando. Para cada subproceso, tiene acceso a la ubicación global de un elemento de datos en relación con el objeto array
o array_view
completo y acceso a la ubicación local relativa al mosaico. El uso del valor de índice local simplifica el código porque no es necesario escribir el código para traducir los valores de índice de global a local. Para usar mosaicos, llame al método extent::tile en el dominio de proceso del método parallel_for_each
y use un objeto tiled_index en la expresión lambda.
En las aplicaciones típicas, los elementos de un mosaico están relacionados de alguna manera, y el código tiene que acceder a los valores del mosaico y llevar un seguimiento de ellos. Use la palabra clave tile_static y el método tile_barrier::wait para lograrlo. Una variable con la palabra clave tile_static tiene un ámbito en un mosaico completo, y se crea una instancia de la variable para cada mosaico. Debe controlar la sincronización con la variable del acceso a subprocesos del mosaico. El método tile_barrier::wait detiene la ejecución del subproceso actual hasta que todos los subprocesos del mosaico hayan alcanzado la llamada a tile_barrier::wait
. Por lo tanto, puede acumular valores en el mosaico mediante el uso de variables tile_static. Después, puede finalizar los cálculos que requieran acceso a todos los valores.
El diagrama siguiente representa una matriz bidimensional de datos de muestreo organizados en mosaicos.
En el ejemplo de código siguiente se usan los datos de muestreo del diagrama anterior. El código reemplaza cada valor del mosaico por el promedio de valores del mosaico.
// Sample data:
int sampledata[] = {
2, 2, 9, 7, 1, 4,
4, 4, 8, 8, 3, 4,
1, 5, 1, 2, 5, 2,
6, 8, 3, 2, 7, 2};
// The tiles:
// 2 2 9 7 1 4
// 4 4 8 8 3 4
//
// 1 5 1 2 5 2
// 6 8 3 2 7 2
// Averages:
int averagedata[] = {
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0,
};
array_view<int, 2> sample(4, 6, sampledata);
array_view<int, 2> average(4, 6, averagedata);
parallel_for_each(
// Create threads for sample.extent and divide the extent into 2 x 2 tiles.
sample.extent.tile<2,2>(),
[=](tiled_index<2,2> idx) restrict(amp) {
// Create a 2 x 2 array to hold the values in this tile.
tile_static int nums[2][2];
// Copy the values for the tile into the 2 x 2 array.
nums[idx.local[1]][idx.local[0]] = sample[idx.global];
// When all the threads have executed and the 2 x 2 array is complete, find the average.
idx.barrier.wait();
int sum = nums[0][0] + nums[0][1] + nums[1][0] + nums[1][1];
// Copy the average into the array_view.
average[idx.global] = sum / 4;
});
for (int i = 0; i <4; i++) {
for (int j = 0; j <6; j++) {
std::cout << average(i,j) << " ";
}
std::cout << "\n";
}
// Output:
// 3 3 8 8 3 3
// 3 3 8 8 3 3
// 5 5 2 2 4 4
// 5 5 2 2 4 4
Bibliotecas matemáticas
C++ AMP incluye dos bibliotecas matemáticas. La biblioteca de precisión doble del espacio de nombres Concurrency::precise_math proporciona compatibilidad con funciones de precisión doble. También proporciona compatibilidad con funciones de precisión sencilla, aunque la compatibilidad de precisión doble en el hardware sigue siendo necesaria. Se ajusta a los requisitos de la especificación C99 (ISO/IEC 9899). El acelerador debe admitir una precisión doble completa. Para determinar si lo hace, compruebe el valor del miembro de datos accelerator::supports_double_precision. La biblioteca matemática rápida, en el espacio de nombres Concurrency::fast_math, contiene otro conjunto de funciones matemáticas. Estas funciones, que solo float
admiten operandos, se ejecutan más rápidamente, pero no son tan precisas como las de la biblioteca matemática de precisión doble. Las funciones se encuentran en el archivo de encabezado <amp_math.h> y todas se declaran con restrict(amp)
. Las funciones del archivo de encabezado <cmath> se importan en los espacios de nombres fast_math
y precise_math
. La palabra clave restrict
se usa para distinguir la versión de <cmath> y la versión de C++ AMP. El código siguiente calcula el logaritmo de base 10 (mediante el método rápido) de cada valor que se encuentra en el dominio de proceso.
#include <amp.h>
#include <amp_math.h>
#include <iostream>
using namespace concurrency;
void MathExample() {
double numbers[] = { 1.0, 10.0, 60.0, 100.0, 600.0, 1000.0 };
array_view<double, 1> logs(6, numbers);
parallel_for_each(
logs.extent,
[=] (index<1> idx) restrict(amp) {
logs[idx] = concurrency::fast_math::log10(numbers[idx]);
}
);
for (int i = 0; i < 6; i++) {
std::cout << logs[i] << "\n";
}
}
Biblioteca de gráficos
C++ AMP incluye una biblioteca de gráficos diseñada para la programación de gráficos acelerada. Esta biblioteca solo se usa en dispositivos que admiten la funcionalidad de gráficos nativos. Los métodos se encuentran en el espacio de nombres Concurrency::graphics y se incluyen en el archivo de encabezado <amp_graphics.h>. Estos son los componentes clave de la biblioteca de gráficos:
Clase texture: puede usar la clase texture para crear texturas a partir de la memoria o de un archivo. Las texturas son similares a las matrices porque contienen datos, y se asemejan a los contenedores de la biblioteca estándar de C++ en lo que respecta a la asignación y la construcción de copias. Para más información, vea Contenedores de la biblioteca estándar de C++. Los parámetros de plantilla de la clase
texture
son el tipo de elemento y la clasificación. La clasificación puede ser 1, 2 o 3. El tipo de elemento puede ser uno de los tipos de vectores cortos que se describen más adelante en este artículo.Clase writeonly_texture_view: proporciona acceso de solo escritura a cualquier textura.
Biblioteca de vectores cortos: define un conjunto de tipos de vectores cortos de longitud 2, 3 y 4 que se basan en
int
,uint
,float
,double
, norm o unorm.
Aplicaciones de Plataforma universal de Windows (UWP)
Al igual que otras bibliotecas de C++, se puede usar C++ AMP en las aplicaciones para UWP. En estos artículos se describe cómo incluir código de C++ AMP en aplicaciones creadas mediante C++, C#, Visual Basic o JavaScript:
C++ AMP y el visualizador de simultaneidad
El visualizador de simultaneidad incluye compatibilidad para analizar el rendimiento de código de C++ AMP. En estos artículos se describen estas características:
Recomendaciones de rendimiento
El módulo y la división de enteros sin signo tienen un rendimiento considerablemente mejor que el módulo y la división de enteros con signo. Se recomienda usar enteros sin signo siempre que sea posible.
Consulte también
C++ AMP (C++ Accelerated Massive Parallelism)
Sintaxis de la expresión lambda
Referencia (C++ AMP)
Blog de programación en paralelo en código nativo