Comparteix a través de


Introducción a 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.

El paralelismo masivo acelerado de C++ (C++ AMP) acelera la ejecución del código de C++ aprovechando el hardware paralelo de datos, como una unidad de procesamiento de gráficos (GPU) en una tarjeta gráfica discreta. Mediante C++ AMP, puede codificar algoritmos de datos multidimensionales para que la ejecución se pueda acelerar mediante paralelismo en hardware heterogéneo. El modelo de programación de C++ AMP incluye matrices multidimensionales, indexación, transferencia de memoria, mosaicos y una biblioteca de funciones matemáticas. Puede usar extensiones de lenguaje C++ AMP para controlar cómo se mueven los datos desde la CPU a la GPU y hacia atrás, de modo que pueda mejorar el rendimiento.

Requisitos del sistema

  • Windows 7 o posterior

  • Windows Server 2008 R2 hasta Visual Studio 2019.

  • Hardware con Nivel de Funcionalidad 11.0 o superior de DirectX 11

  • Para la depuración en el emulador de software, se requiere Windows 8 o Windows Server 2012. Para la depuración del hardware, debe instalar los controladores de la 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 desea agregar los elementos correspondientes de dos matrices unidimensionales. Por ejemplo, puede que quiera agregar {1, 2, 3, 4, 5} y {6, 7, 8, 9, 10} para obtener {7, 9, 11, 13, 15}. Sin usar C++ AMP, puede escribir el código siguiente para agregar 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: los datos constan de tres matrices. Todos tienen el mismo rango (uno) y longitud (cinco).

  • Iteración: el primer for bucle proporciona un mecanismo para recorrer en iteración los elementos de las matrices. El código que desea ejecutar para calcular las sumas se encuentra en el primer for bloque.

  • Índice: la idx variable tiene acceso a los elementos individuales de las matrices.

Con C++ AMP, puede escribir el código siguiente en su lugar.

#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";
    }
}

Los mismos elementos básicos están presentes, 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 array_view objeto: los valores de datos, el rango, el tipo de elemento y la longitud del array_view objeto en cada dimensión. El rango 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 del array_view objeto 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 array_view clase y se describe más adelante en este artículo.

  • Iteración: la función parallel_for_each (C++ AMP) proporciona un mecanismo para recorrer en iteración los elementos de datos o el dominio de proceso. En este ejemplo, el dominio de cómputo lo especifica sum.extent. El código que desea ejecutar está contenido en una expresión lambda o en una función de kernel. restrict(amp) indica que solo se usa el subconjunto del lenguaje C++ que C++ AMP puede acelerar.

  • Índice: la variable Class de índice , idx, se declara con un rango de uno para que coincida con el rango del array_view objeto. Mediante el índice, puede acceder a los elementos individuales de los array_view objetos .

Modelado e indexación de datos: índice y extensión

Debe definir los valores de 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 puede definir la matriz para que tenga cualquier rango (número de dimensiones). Los datos pueden tener cualquier tamaño en cualquiera de las dimensiones.

índice Clase

El índice Class especifica una ubicación en un objeto array o array_view al encapsular el desplazamiento desde el origen en cada dimensión en un solo objeto. Cuando se obtiene acceso a una ubicación de la matriz, se pasa un index objeto 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 unidimensional array_view . El índice se usa para imprimir el tercer elemento del array_view objeto . La salida 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 = 1 y la columna = 2 en un objeto bidimensional array_view . El primer parámetro del index constructor es el componente de fila y el segundo es el componente de columna. La salida 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 = 0, la fila = 1 y la columna = 3 en un objeto tridimensional array_view . Observe que el primer parámetro es el componente de profundidad, el segundo parámetro es el componente de fila y el tercer parámetro es el componente de columna. La salida 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

clase de extensión

La clase Extent especifica la longitud de los datos de cada dimensión del objeto array o array_view. Puede crear una extensión y usarla para crear un array objeto o array_view . También puede recuperar la extensión de un array o array_view objeto existente. En el ejemplo siguiente se imprime la longitud de la extensión en cada dimensión de un array_view objeto.

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 array_view objeto que tiene las mismas dimensiones que el objeto en el ejemplo anterior, pero en este ejemplo se usa un extent objeto en lugar de usar parámetros explícitos en el array_view constructor.

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";

Mover datos al acelerador: matriz y array_view

Dos contenedores de datos usados para mover datos al acelerador se definen en la biblioteca en tiempo de ejecución. Son la clase de matriz y la clase array_view. La array clase es una clase contenedora que crea una copia profunda de los datos cuando se construye el objeto. La array_view clase es una clase contenedora que copia los datos cuando la función de 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 array objeto, se crea una copia profunda de los datos en el acelerador si usa un constructor que incluye un puntero al conjunto de datos. La función del núcleo modifica la copia en el acelerador. Cuando finalice la ejecución de la función kernel, debe volver a copiar los datos en la estructura de datos de origen. En el ejemplo siguiente se multiplica cada elemento de un vector en 10. Una vez finalizada la función de kernel, vector conversion operator se usa para copiar los datos de nuevo 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";
}

Clase array_view

array_view tiene casi los mismos miembros que la array clase , pero el comportamiento subyacente no es el mismo. Los datos pasados al array_view constructor no se replican en la GPU, como ocurre con un array constructor. En su lugar, los datos se copian en el acelerador cuando se ejecuta la función de kernel. Por lo tanto, si crea dos array_view objetos que usan los mismos datos, ambos array_view objetos hacen referencia al mismo espacio de memoria. Al hacerlo, debe sincronizar cualquier acceso multiproceso. La principal ventaja de usar la array_view clase 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 diferencias entre las array clases y array_view .

Descripción clase array clase array_view
Cuando se determina la clasificación En tiempo de compilación. En tiempo de compilación.
Cuando 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 envoltorio de datos.
Copy Copia explícita y profunda durante la definición. Copia implícita cuando la función kernel accede a ella.
Recuperación de datos Copiando los datos de la matriz en un objeto en el subproceso de CPU. Mediante el acceso directo del array_view objeto o llamando al método array_view::synchronize para seguir accediendo a los datos del contenedor original.

Memoria compartida con matriz y array_view

La memoria compartida es la memoria a la que puede acceder tanto la CPU como el acelerador. El uso de memoria compartida elimina o reduce significativamente la sobrecarga de copiar datos entre la CPU y el acelerador. Aunque la memoria se comparte, tanto la CPU como el acelerador no pueden tener acceso a ella simultáneamente y, al hacerlo, se produce un comportamiento indefinido.

array Los objetos se pueden usar para especificar un control específico 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 default_cpu_access_type predeterminada para las asignaciones de memoria en el acelerador está determinada por la propiedad . De forma predeterminada, los objetos array adoptan el mismo access_type que el asociado principal accelerator.

Al establecer la propiedad array::cpu_access_type Data Member de un objeto 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 el mismo cpu_access_type que el array con el que está asociado; o, si el array_view se construye sin un origen de datos, su access_type refleja el entorno que primero hace que se asigne almacenamiento. Es decir, si el host (CPU) accede por primera vez, se comporta como si se hubiera creado a partir de un origen de datos de CPU y comparte el access_type del accelerator_view asociado por captura; sin embargo, si un accelerator_view accede por primera vez, se comporta como si se hubiera creado sobre un array creado en ese accelerator_view y comparte el arrayaccess_type del objeto.

En el ejemplo de código siguiente se muestra cómo determinar si el acelerador predeterminado admite memoria compartida y, a continuación, crea 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 a través de datos: parallel_for_each

La función parallel_for_each define el código que desea ejecutar en el acelerador con los datos del array objeto o array_view . Tenga en cuenta 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 parallel_for_each método toma dos argumentos, un dominio de proceso y una expresión lambda.

El dominio de proceso es un extent objeto o un tiled_extent objeto 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 extent objeto 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 capture, [=], especifica que el cuerpo de la expresión lambda tiene acceso a todas las variables capturadas por valor, que en este caso son a, by sum. En este ejemplo, la lista de parámetros crea una variable unidimensional index 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 puede acelerar. Las limitaciones de las funciones que tienen el modificador restrict se describen en restrict (C++ AMP). Para obtener más información, vea Sintaxis de expresión lambda.

La expresión lambda puede incluir el código que se va a ejecutar o puede llamar a una función de kernel independiente. La función kernel debe incluir el restrict(amp) modificador . El ejemplo siguiente es equivalente al ejemplo anterior, pero llama a una función de 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 aceleración adicional mediante el uso de mosaicos. El término "Tiling" divide los subprocesos en subconjuntos rectangulares iguales o bloques. Determine el tamaño de icono adecuado en función del conjunto de datos y el algoritmo que está codificando. Para cada subproceso, tiene acceso a la ubicación global de un elemento de datos en relación con todo array o array_view y acceso a la ubicación local relativa al icono. El uso del valor de índice local simplifica el código porque no tiene que 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 cálculo dentro del método parallel_for_each, y utilice un objeto tiled_index en la expresión lambda.

En las aplicaciones típicas, los elementos de un icono están relacionados de alguna manera, y el código tiene que acceder y realizar un seguimiento de los valores en el icono. Utilice la palabra clave tile_static y el método tile_barrier::wait para lograrlo. Una variable que tiene la palabra clave tile_static tiene un ámbito en un icono completo y se crea una instancia de la variable para cada icono. Debe gestionar la sincronización del acceso de los hilos del mosaico a la variable. El método tile_barrier::wait detiene la ejecución del subproceso actual hasta que todos los subprocesos del icono hayan alcanzado la llamada a tile_barrier::wait. Por lo tanto, puede acumular valores en el bloque utilizando variables tile_static. A continuación, 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.

Valores de índice en una extensión en mosaico.

En el ejemplo de código siguiente se usan los datos de muestreo del diagrama anterior. El código reemplaza cada valor del icono por el promedio de los valores del icono.

// 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 en el espacio de nombres Concurrency::precise_math ofrece soporte para funciones de precisión doble. También proporciona compatibilidad con funciones de precisión única, aunque todavía se requiere compatibilidad con precisión doble en el hardware. Se ajusta a la especificación C99 (ISO/IEC 9899). El acelerador debe admitir una precisión doble completa. Puede determinar si lo hace comprobando el valor del accelerator::supports_double_precision miembro de datos. La biblioteca de matemáticas rápidas, dentro del espacio de nombres Concurrency::fast_math, contiene otro conjunto de funciones matemáticas. Estas funciones, que admiten solo float operandos, se ejecutan más rápidamente, pero no son tan precisas como las de la biblioteca matemática de doble precisión. Las funciones están contenidas en el <archivo de encabezado amp_math.h> y todas se declaran con restrict(amp). Las funciones en el <archivo de encabezado cmath> se importan en los fast_math espacios de nombres y en los precise_math. La restrict palabra clave se usa para distinguir la <versión cmath> y la versión de C++ AMP. El código siguiente calcula el logaritmo 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 están en el espacio de nombres Concurrency::graphics y están contenidos en el <archivo de encabezado amp_graphics.h>. Los componentes clave de la biblioteca de gráficos son:

  • clase Texture: puede usar la clase texture para crear texturas a partir de la memoria o de un archivo. Las texturas se asemejan a las matrices porque contienen datos y se asemejan a los contenedores de la biblioteca estándar de C++ con respecto a la asignación y la construcción de la copia. Para obtener más información, consulte Contenedores de biblioteca estándar de C++. Los parámetros de plantilla de la texture clase son el tipo de elemento y el rango. El rango puede ser 1, 2 o 3. El tipo de elemento puede ser uno de los tipos de vector corto 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 cortas: define un conjunto de tipos de vector corto de longitud 2, 3 y 4 basados en int, uint, float, double, norm o unorm.

Aplicaciones para la Plataforma universal de Windows (UWP)

Al igual que otras bibliotecas de C++, puedes usar C++ AMP en tus 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:

AMP de C++ y Visualizador de concurrencia

El visualizador de simultaneidad ofrece soporte para el análisis del rendimiento del 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 significativamente 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 (Paralelismo masivo acelerado de C++)
Sintaxis de expresión lambda
Referencia (C++ AMP)
Blog de programación en paralelo en código nativo