Compartir a través de


Información general sobre C++ AMP

C++ Accelerated Massive Parallelism (C++ AMP) acelera la ejecución del código C++ aprovechándose del hardware de paralelismo de datos, por ejemplo una unidad de procesamiento gráfico (GPU), en una tarjeta gráfica discreta. Al utilizar C++ AMP, se pueden codificar algoritmos para datos multidimensionales de modo que se acelere la ejecución mediante el uso del paralelismo en hardware heterogéneo. El modelo de programación C++ AMP incluye matrices multidimensionales, indización, transferencia de memoria, mosaicos y una biblioteca de funciones matemáticas. Puede utilizar extensiones de lenguaje de C++ AMP para controlar cómo pasan los datos de la CPU a la GPU y viceversa, para mejorar el rendimiento.

Requisitos del sistema

  • Windows 7, Windows 8, Windows Server 2008 R2 o Windows Server 2012

  • Nivel de característica 11.0 de DirectX 11 o hardware posterior

  • Para depurar 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, vea Depurar código de GPU.

Introducción

Los siguientes dos ejemplos muestran los componentes principales de C++ AMP. Suponga que desea agregar los elementos correspondientes de dos matrices unidimensionales. Por ejemplo, quizás desee añadir {1, 2, 3, 4, 5} y {6, 7, 8, 9, 10} para obtener {7, 9, 11, 13, 15}. Sin utilizar C++ AMP, se podría escribir el siguiente código 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 de código son las siguientes:

  • Datos: Los datos se componen de tres matrices. Todos tienen el mismo rango (uno) y la misma longitud (cinco).

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

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

Mediante C++ AMP, podría escribir en su lugar el siguiente código.

#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 utilizan las construcciones de C++ AMP:

  • Datos: Se utilizan las matrices de C++ para construir tres objetos de array_view de C++ AMP. Se proporcionan cuatro valores para construir un objeto array_view: los valores de datos, el rango, el tipo de elemento y la longitud del objeto array_view 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 del constructor. En este ejemplo, la matriz de C++ que se pasa al constructor es unidimensional. Se usan el rango y la longitud para construir la forma rectangular de los datos en el objeto array_view y los valores de datos se utilizan para rellenar la matriz. La biblioteca de runtime también incluye array (Clase), que tiene una interfaz similar a la clase array_view y que se describe más adelante en este artículo.

  • Iteración: parallel_for_each (Función) (C++ AMP) proporciona un mecanismo para recorrer en iteración los datos, o el dominio del cálculo. En este ejemplo, el dominio del cálculo viene especificado por sum.extent. El código que se desea ejecutar se encuentra en una expresión lambda o función kernel. restrict(amp) indica que solo se utiliza el subconjunto del lenguaje C++ que el C++ AMP puede acelerar.

  • Índice: La variable de index (Clase) , idx, se declara con rango de uno para corresponder al rango del objeto array_view. Utilizando el índice,se puede tener acceso a los elementos individuales de los objetos de array_view.

Dando forma e indexando datos: índice y extensión

Debe definir los valores de los datos y declarar la forma de los datos antes de poder ejecutar el código del kernel. Todos los datos se definen como una matriz (rectangular), y se 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)

index (Clase) especifica una ubicación en el objeto array o array_view encapsulando el desplazamiento desde el origen en cada dimensión en un objeto. Cuando se obtiene acceso a una ubicación en la matriz, se pasa un objeto index al operador de indexación, [], en lugar de una lista de índices enteros. Puede tener acceso a los elementos de cada dimensión a través de array::operator() (Operador) o de array_view::operator() (Operador).

El siguiente ejemplo crea un índice unidimensional que especifica el tercer elemento de un objeto unidimensional array_view. El índice se utiliza 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

El ejemplo siguiente crea un índice bidimensional que especifica el elemento en el cual la fila = 1 y la columna = 2 en un objeto bidimensional array_view. El primer parámetro del constructor index es el componente de la fila y el segundo parámetro es el componente de la 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

El ejemplo siguiente crea un índice tridimensional que especifica el elemento en el que la profundidad = 0, el rango = 1, y la columna = 3 en un objeto tridimensional de array_view. Observe que el primer parámetro es la profundidad, el segundo parámetro es la fila y el tercer parámetro es la 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

clase extent

extent (Clase) (C++ AMP) especifica la longitud de los datos en cada dimensión del objeto array o array_view. Se puede crear una extensión y utilizarla para crear un objeto array o un objeto array_view. También se puede recuperar la extensión de un objeto array o array_view existente. El ejemplo siguiente 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";

El siguiente ejemplo crea un objeto array_view que tiene las mismas dimensiones que el objeto del ejemplo anterior, pero este ejemplo utiliza un objeto extent en lugar de utilizar parámetros explícitos en el constructor de 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";

Mover datos al Acelerador: array y array_view

En la biblioteca de runtime se definen dos contenedores de datos que se utilizan para mover datos al acelerador. Estos son array (Clase) y array_view (Clase). La clase array es una clase contenedora que crea una copia profunda de datos cuando se construye el objeto. La clase array_view es una clase contenedora que copia los datos cuando la función de kernel tiene acceso a los datos. Cuando los datos son necesarios en el dispositivo de origen los datos se vuelven a copiar.

Clase array

Cuando se construye un objeto array, se crea en el acelerador una copia en profundidad de los datos si se utiliza un constructor que incluya un puntero al conjunto de los datos. La función kernel modifica la copia en el acelerador. Cuando la ejecución de la función kernel finaliza, se deben copiar los datos en la estructura de datos de código fuente. El ejemplo siguiente multiplica cada elemento de un vector por 10. Una vez finalizada la función kernel, el operador de conversión de vectores se utiliza para copiar los datos en el objeto vector.

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

La array_view tiene casi los mismos miembros que la clase de array, pero el comportamiento subyacente no es el mismo. Los datos que se pasan al constructor array_view no se replican en la GPU como se replican con un constructor array. De hecho,los datos se copian al acelerador cuando se ejecuta la función kernel. Por consiguiente, si se crean dos objetos array_view que utilizan los mismos datos, ambos objetos array_view harán referencia al mismo espacio de memoria. Si lo hace así, tendrá que sincronizar cualquier acceso multiproceso. La ventaja principal de utilizar la clase array_view es que los datos se mueven solo si es necesario.

Comparación de array y array_view

La tabla siguiente resume las similitudes y diferencias que hay entre las clases array y array_view.

Descripción

clase array

clase array_view

Cuando se determina el rango

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.

Copiar

Copia explícita y en profundidad en la definición.

Copia implícita cuando se tiene acceso a la misma con la función kernel.

Recuperación de datos

Copiar los datos de la matriz de nuevo a un objeto CPU subproceso.

Mediante el acceso directo array_view o llamando a array_view::synchronize (Método) para continuar teniendo acceso a los datos en el contenedor original.

Memoria compartida con matriz y array_view

La memoria compartida es memoria a la que pueden tener acceso la CPU y el acelerador. El uso de memoria compartida elimina o reduce de manera significativa la sobrecarga de la copia de datos entre la CPU y el acelerador. Aunque la memoria se comparte, no es posible el acceso simultáneo por parte de la CPU y del acelerador y, si se hace así, se produce un comportamiento indefinido.

Se pueden usar objetos array para especificar un control preciso 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 una memoria compartida, el access_type (Enumeración) predeterminado de las asignaciones de memoria en el acelerador viene determinado por la propiedad default_cpu_access_type. De manera predeterminada, los objetos array y array_view aceptan el mismo access_type que el accelerator asociado principal.

Si establece la propiedad array::cpu_access_type (Miembro de datos) de una array explícitamente, puede controlar con precisión cómo se utiliza la memoria compartida, para poder optimizar la aplicación para características de rendimiento de hardware, según los patrones de acceso de memoria de los núcleos de cálculo. Una array_view refleja el mismo cpu_access_type que la array a la que está asociada; o bien, si la array_view se construye sin un origen de datos, su access_type refleja el entorno que provoca por primera vez la asignación de almacenamiento. Es decir, si el host (CPU) tiene acceso en primer lugar, se comporta como si se hubiera creado en un origen de datos de CPU y comparte el access_type de accelerator_view asociado por captura; sin embargo, si un objeto accelerator_view tiene acceso en primer lugar, entonces se comporta como si se hubiera creado en un elemento array creado en dicho objeto accelerator_view y comparte el access_type de array.

El ejemplo de código siguiente muestra cómo determinar si el acelerador predeterminado admite memoria compartida y, a continuación, crea varias matrices con distintas configuraciones de cpu_access_type.

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

Ejecutando código sobre los datos: parallel_for_each

La función parallel_for_each define el código que se desea ejecutar en el acelerador contra los datos del objeto array o array_view. Considere el siguiente código proveniente 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 cálculo y una expresión lambda.

El dominio del cálculo es un objeto extent o un objeto tiled_extent que define el conjunto de subprocesos que hay que crear para la ejecución en paralelo. Se genera un subproceso para cada elemento en el dominio del cálculo. En este caso, el objeto extent es unidimensional y tiene cinco elementos. Por consiguiente, se inician cinco subprocesos.

La expresión lambda define el código que hay que ejecutar en cada subproceso. La cláusula de captura, [=], especifica que el cuerpo de la expresión lambda tiene acceso 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 unidimensional index denominada idx. El valor de idx[0] es 0 en el primer subproceso y aumenta en uno en cada subproceso siguiente. restrict(amp) indica que solo se utiliza el subconjunto del lenguaje C++ que el C++ AMP puede acelerar. Las limitaciones de las funciones con el modificador limitado se describen en Cláusula de restricción (AMP de C++). Para obtener más información, vea Sintaxis de las expresiones lambda.

La expresión lambda puede incluir código para ejecutar o puede llamar a una función independiente del kernel. La función kernel debe incluir el modificador restrict(amp). El siguiente ejemplo 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";
    }
}

Acelerar código: Teselas y Barreras

Puede aprovechar la aceleración adicional utilizando mosaicos. El mosaico divide los subprocesos en subconjuntos rectangulares iguales o mosaicos. Determine el tamaño apropiado del mosaico según el conjunto de datos y el algoritmo que se está escribiendo. Para cada subproceso, se tiene acceso a la ubicación global de un elemento de datos en relación con todo array o array_view y el acceso a la ubicación local en relación con la tesela. Al utilizar el valor de índice local se simplifica el código porque no hay que escribir el código para convertir valores de índices globales a locales. Para utilizar el teselado, llame a extent::tile (Método) en el dominio del cálculo en el método parallel_for_each , y utilice un objeto tiled_index en la expresión lambda.

En las aplicaciones típicas, los elementos de una tesela están en cierto modo relacionados, y el código tiene que tener acceso y poder seguir los valores a través del mosaico. Use la palabra clave palabra clave tile_static y tile_barrier::wait (Método) para lograrlo. Una variable que tiene la palabra clave tile_static tiene un ámbito a través de una tesela completa, y por cada tesela se crea una instancia de la variable. Debe controlar la sincronización del acceso del subproceso del mosaico a la variable. tile_barrier::wait (Método) detiene la ejecución del subproceso actual hasta que todos los subprocesos del mosaico hayan alcanzado la llamada a tile_barrier::wait. De forma que se puedan acumular los valores a través de la tesela mediante las variables tile_static. A continuación se puede finalizar cualquier cálculo que requiera acceso a todos los valores.

El diagrama siguiente representa una matriz bidimensional de los datos de muestreo que se organiza en teselas.

Valores de índice en una extensión en mosaico

El siguiente ejemplo de código utiliza los datos de muestreo del diagrama anterior. El código reemplaza cada valor de la tesela por el promedio de los valores de la tesela.

// 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 doble precisión de Concurrency::precise_math (Espacio de nombres) proporciona compatibilidad para las funciones de doble precisión. También proporciona compatibilidad a las funciones de precisión simple, aunque todavía se requiere la compatibilidad de doble precisión en hardware. Cumple la especificación C99 (ISO/IEC 9899). El acelerador debe admitir doble precisión completa. Puede determinar si lo hace comprobando el valor de accelerator::supports_double_precision (Miembro de datos). La librería matemática rápida, en Concurrency::fast_math (Espacio de nombres), contiene otro conjunto de funciones matemáticas. Estas funciones, que solo admiten los operandos float, se ejecutan más rápidamente pero no son tan exactas como las de la biblioteca matemática de doble precisión. Las funciones se incluyen en el archivo de encabezado <amp_math.h> y todas se declaran con restrict(amp). Las funciones del archivo de encabezado de <cmath> se importan en los espacios de nombres fast_math y precise_math. La palabra clave restrict se usa para distinguir la versión <cmath> y la versión C++ AMP. El código siguiente calcula el logaritmo en base 10, mediante el método rápido, de cada valor comprendido en el dominio de cálculo.

#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(logs[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 que está diseñada para la programación de gráficos acelerados. Esta biblioteca se utiliza solo en los dispositivos que admiten funcionalidad de gráficos nativos. Los métodos están en el Concurrency::graphics (Espacio de nombres) y están incluidos en el archivo de encabezado de <amp_graphics.h>. Los componentes clave de la biblioteca de gráficos son:

  • texture (Clase): se puede utilizar la clase textura para crear texturas desde la memoria o desde un archivo. Las texturas se parecen a las matrices porque contienen datos y se parecen a los contenedores de la biblioteca de plantillas estándar (STL) en lo que se refiere la asignación y construcción de copia. Para obtener más información, vea Contenedores de STL. Los parámetros de la plantilla para la clase texture 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.

  • writeonly_texture_view (Clase): proporciona acceso de solo escritura a cualquier textura.

  • Biblioteca corta de vector: Define un conjunto de tipos de vector corto de longitud 2, 3 y 4, basados en int, uint, float, double, norm, o unorm.

Aplicaciones de Tienda Windows

Como otras bibliotecas de C++, puede utilizar C++ AMP en sus aplicaciones de Tienda Windows. Estos artículos describen cómo incluir código de C++ AMP en aplicaciones que se crean con C++, C#, Visual Basic o JavaScript:

C++ AMP y Visualizador de simultaneidad

El visualizador de simultaneidad incluye compatibilidad para analizar el rendimiento del código C++ AMP. Estos artículos describen estas características:

Recomendaciones acerca del rendimiento

El módulo y la división de enteros sin signo tienen un rendimiento mucho mejor que el módulo y la división de enteros con signo. Se recomienda usar enteros sin signo cuando sea posible.

Vea también

Referencia

Sintaxis de las expresiones lambda

Otros recursos

C++ AMP (C++ Accelerated Massive Parallelism)

Referencia (C++ AMP)

Programación paralela en el blog de código nativo