C++

Introducción basada en códigos de C++ AMP

Daniel Moth

Descargar el ejemplo de código

Este artículo abarca una tecnología preliminar llamada C++ AMP que se entregará con Visual Studio 2011. Toda la información está sujeta a cambio.

Visual Studio 11 lleva la compatibilidad para informática heterogénea a los usuarios comunes y corrientes a través de una tecnología llamada C++ Accelerated Massive Parallelism (C++ AMP). Esto le permite aprovechar los aceleradores, como GPU, para acelerar los algoritmos paralelos de datos.

C++ AMP ofrece rendimiento, de manera similar al hardware portátil, sin comprometer la productividad que se espera de un moderno C++ y el paquete de Visual Studio. Podría aumentar considerablemente la velocidad si se compara con solo usar la CPU. En los congresos, normalmente hago una demostración de un solo proceso aprovechando las GPU de NVIDIA y AMD al mismo tiempo, mientras sigo teniendo una solución de reserva de CPU.

En esta introducción estructurada por código que explora C++ AMP, supondré que lee cada una de las líneas de código que aparecen en el artículo. El código en línea es una parte central del artículo y lo que está en el código C++ no se repetirá necesariamente en el texto del artículo.

Configuración y algoritmo de ejemplo

Primero, comprendamos el algoritmo simple con el que trabajaremos, además del código de configuración necesario, en la preparación para la conversión para usar más tarde C++ AMP.

Cree un proyecto C++ vacío, agregue un archivo C++ nuevo vacío (Source.cpp) y escriba el siguiente código que se explica por sí solo (uso números de línea no contiguos para facilitar la explicación en el texto del artículo y verá los mismos números de línea en el proyecto descargable que acompaña):

1 #include <amp.h>                // C++ AMP header file
3 #include <iostream>             // For std::cout etc
4 using namespace concurrency;    // Save some typing :)
5 using std::vector;     // Ditto. Comes from <vector> brought in by amp.h
6
79 int main()
80 {
81   do_it();
82
83   std::cout << "Hit any key to exit..." << std::endl;
84   std::cin.get();
85 }

C++ AMP introduce una variedad de tipos en una variedad de archivos de encabezado. Según las líneas 1 y 4 en el fragmento de código anterior, el archivo de encabezado principal es amp.h y los tipos principales se agregan al espacio de nombres de simultaneidad existente. No se requiere una opción de compilación o configuración adicional para usar C++ AMP. Agreguemos una función do_it sobre principal (consulte la figura 1).

Figura 1 La función do_it Function llamada desde principal

52 void do_it()
53 {
54   // Rows and columns for matrix
55   const int M = 1024;
56   const int N = 1024;
57
58   // Create storage for a matrix of above size
59   vector<int> vA(M * N);
60   vector<int> vB(M * N);
61
62   // Populate matrix objects
63   int i = 0;
64   std::generate(vA.begin(), vA.end(), [&i](){return i++;});
65   std::generate(vB.begin(), vB.end(), [&i](){return i--;});
66
67   // Output storage for matrix calculation
68   vector<int> vC(M * N);
69
70   perform_calculation(vA, vB, vC, M, N);
76 }

En las líneas 59, 60 y 68, el código usa objetos std::vector como contenedores planos para cada matriz, aun cuando un tipo de dos dimensiones es con lo que realmente quisiera estar tratando. Hablaremos más sobre esto más adelante.

Es importante comprender el uso de las expresiones lambda en las líneas 64 y 65 que se transmiten al método std::generate para rellenar los dos objetos vectoriales. Este artículo supone que puede usar lambdas en C++ competentemente. Por ejemplo, debe comprender instantáneamente que si la variable i se capturó por valor (al modificar la lista de capturas así [i] o así [=] y usando la palabras clave mutable), cada miembro del vector se habría inicializado en 0! Si no se siente cómodo con el uso de lambdas (una adición maravillosa al C++ 11 estándar), lea el artículo de MSDN Library titulado “Expresiones lambda en C++” (msdn.microsoft.com/library/dd293608) y vuelva cuando termine.

La función do_it introdujo una llamada a perform_calculation, que se codifica de la siguiente manera:

7  void perform_calculation(
8    vector<int>& vA, vector<int>& vB, vector<int>& vC, int M, int N)
9  {
15   for (int i = 0; i < M; i++)
16   {
17     for (int j = 0; j < N; j++)
18     {
19       vC[i * N + j] = vA[i * N + j] + vB[i * N + j];
20     }
22   }
24 }

En este ejemplo sencillo de adición de matriz, algo que sobresale es que la multidimensionalidad de la matriz se pierde debido al almacenamiento linealizado de la matriz en un objeto vectorial (que es la razón por la que tuvo que pasar en las dimensiones de la matriz, junto con los objetos vectoriales). Además, debe realizar un cálculo extraño con los índices que aparecen en la línea 19. Este punto podría ser incluso más obvio si deseara agregar submatrices de estas matrices en conjunto.

Hasta el momento no hemos tenido código de C++ AMP. A continuación, si se cambia la función perform_calculation, verá cómo puede comenzar a introducir algunos de los tipos de C++ AMP. En secciones posteriores, aprender a aprovechar C++ AMP y acelerar sus algoritmos paralelos de datos.

array_view<T, N>, extent<N> e index<N>

C++ AMP introduce un tipo concurrency::array_view para encapsular contenedores de datos; puede considerarlo como un puntero inteligente. Representa los datos de manera rectangular, contiguos en la dimensión menos significativa. El motivo de su existencia resultará obvio más adelante y, a continuación, verá algunos aspectos de su uso. Cambiemos el cuerpo de la función perform_calculation de la siguiente manera:

11     array_view<int> a(M*N, vA), b(M*N, vB);
12     array_view<int> c(M*N, vC);
14
15     for (int i = 0; i < M; i++)
16     {
17       for (int j = 0; j < N; j++)
18       {
19         c(i * N + j) = a(i * N + j) + b(i * N + j);
20       }
22     }

Esta función, que se compila y ejecuta en la CPU, tiene el mismo resultado anterior. La única diferencia es el uso gratuito de los objetos array_view que se introducen en las líneas 11 y 12. La línea 19 sigue teniendo el índice extraño (por ahora), pero ahora usa los objetos array_view (a, b, c) en lugar de los objetos vectoriales (vA, vB y vC) y obtiene acceso a los elementos a través del operador de la función array_view (en contraste con el uso anterior del operador de subscripts vectoriales. Ahondaremos en esto más adelante).

Debe indicar al array_view a través de un argumento de plantilla (en este ejemplo, int) el tipo de elemento del contenedor que encapsula; transmitirá el contenedor como el último argumento de constructor (por ejemplo, la variable vC del tipo vectorial en la línea 12). El primer argumento de constructor es el número de elementos.

También puede especificar el número de elementos con un objeto concurrency::extent, para así poder cambiar las líneas 11 y 12 de la siguiente manera:

10     extent<1> e(M*N);
11     array_view<int, 1> a(e, vA), b(e, vB);
12     array_view<int, 1> c(e, vC);

El objeto extent<N> representa un espacio multidimensional, donde la clasificación se transmite como un argumento de plantilla. En este ejemplo, el argumento de plantilla es 1, pero la clasificación puede ser cualquier valor mayor que cero. El constructor de la extensión acepta el tamaño de cada dimensión que representa el objeto extent, tal como aparece en la línea 10. De ese modo, el objeto extent se puede transmitir al constructor del objeto array_view para definir su forma, como se muestra en las líneas 11 y 12. En esas líneas también agregué un segundo argumento de plantilla al array_view que indica que representa un espacio unidimensional; tal como en el ejemplo de código anterior, podría haber omitido esto sin problemas, porque la clasificación predeterminada es 1.

Ahora que conoce estos tipos, puede realizar otras modificaciones en la función para que pueda obtener acceso a los datos de una manera bidimensional más natural, lo que se asemeja más al mundo de la matriz:

10     extent<2> e(M, N);
11     array_view<int, 2> a(e, vA), b(e, vB);
12     array_view<int, 2> c(e, vC);
14
15     for (int i = 0; i < e[0]; i++)
16     {
17       for (int j = 0; j < e[1]; j++)
18       {
19         c(i, j) = a(i, j) + b(i, j);
20       }
22     }

Los cambios en las líneas 10-12 hacen que los objetos array_view sean bidimensionales, por lo que requeriremos dos índices para obtener acceso a un elemento. Las líneas 15 y 17 obtienen acceso a los límites de la extensión a través de su operador de subscripts en lugar de usar directamente las variables M y N; una vez que haya encapsulado la forma en la extensión, podrá usar ese objeto en todo su código.

El cambio importante es en la línea 19, donde ya no necesita un cálculo extraño. La indización es mucho más natural, lo que hace que todo el algoritmo sea mucho más fácil de leer y mantener.

Si array_view se creó con una extensión tridimensional, el operador de función esperaría tres enteros para obtener acceso a un elemento, nuevamente desde la dimensión más significativa hasta la menos significativa. Como podría esperarse de una API multidimensional, también hay una manera de crear un índice en un array_view a través de un objeto transmitido a su operador de subscripts. El objeto debe ser del tipo concurrency::index<N>, donde N coincide con la clasificación de la extensión en que se crea el array_view. Más adelante verá cómo los objetos de índice se pueden pasar al código, pero por ahora vamos a crear uno de manera manual para conocerlos y verlos en acción, a través de la modificación del cuerpo de la función de la siguiente manera:

10     extent<2> e(M, N);
11     array_view<int, 2> a(e, vA), b(e, vB);
12     array_view<int, 2> c(e, vC);
13
14     index<2> idx(0, 0);
15     for (idx[0] = 0; idx[0] < e[0]; idx[0]++)
16     {
17       for (idx[1] = 0; idx[1] < e[1]; idx[1]++)
18       {
19         c[idx] = a[idx] + b[idx];
//19         //c(idx[0], idx[1]) = a(idx[0], idx[1]) + b(idx[0], idx[1]);
20       }
22     }

Como puede ver en las líneas 14, 15, 17 y 19, el tipo concurrency::index<N> tiene una interfaz muy similar al tipo de extensión, excepto en que el índice representa un punto dimensional N en lugar de un espacio dimensional N. Tanto el tipo de extensión como el tipo de índice admiten un número de operaciones aritméticas a través de la sobrecarga del operador; por ejemplo, la operación de incremento mostrada en el ejemplo anterior.

Anteriormente se usaron las variables de bucle (i y j) para crear un índice en el array_view y ahora se pueden reemplazar con un objeto index en la línea 19. Esto demuestra cómo, a través del uso del operador de subscripts de array_view, puede crear un índice en él con una sola variable (en este ejemplo, idx de tipo index<2>).

En este punto ya tiene un conocimiento básico de los tres nuevos tipos introducidos con C++ AMP: array_view<T,N>, extent<N> e index<N>. Estos tres tipos tienen más para ofrecer, tal como aparece en los diagramas de clase en la figura 2.

array_view, extent and index Classes
Figura 2 Clases array_view, extent e index

El real poder y motivación para usar esta API multidimensional es ejecutar sus algoritmos en un acelerador paralelo de datos, como la GPU. Para hacerlo, necesita un punto de entrada en la API para ejecutar el código en el acelerador, además de una manera de comprobar en tiempo de compilación que se está usando un subconjunto del lenguaje C++ que se puede ejecutar de manera eficiente en dicho acelerador.

parallel_for_each y restrict(amp)

La API que indica al tiempo de ejecución de C++ AMP que tome su función y la ejecute en el acelerador es una nueva sobrecarga para concurrency::parallel_for_each. Acepta dos argumentos: un objeto extent y una expresión lambda.

El objeto extent<N>, con el que ya está familiarizado, se usa para determinar cuántas veces se llamará a la expresión lambda en el acelerador y se debe suponer que cada vez será un subproceso independiente el que llame al código, potencialmente de manera simultánea, sin garantías de orden. Por ejemplo, un extent<1>(5) resultará en cinco llamadas a la expresión lambda que transmite a parallel_for_each, mientras que un extent<2>(3,4) resultará en 12 llamadas a la expresión lambda. En algoritmos reales, normalmente estará programando miles de llamadas a la expresión lambda.

La expresión lambda debe aceptar un objeto index<N> que usted ya conoce. El objeto index debe tener la misma clasificación que el objeto extent que se transmitió a parallel_for_each. Por supuesto, el valor index será diferente cada vez que se llame a la expresión lambda; de esta manera se distinguen dos invocaciones diferentes de la expresión lambda. Podría pensar en el valor index como el identificador del subproceso.

La siguiente es una representación de código de lo descrito hasta ahora con parallel_for_each:

89     extent<2> e(3, 2);
90     parallel_for_each(e,
91       [=](index<2> idx)
92       {
93         // Code that executes on the accelerator.
94         // It gets invoked in parallel by multiple threads
95         // once for each index "contained" in extent e
96         // and the index is passed in via idx.
97         // The following always hold true
98         //      e.rank == idx.rank
99         //      e.contains(idx) == true
100        //      the function gets called e.size() times
101        // For this two-dimensional case (.rank == 2)
102        //      e.size() == 3*2 = 6 threads calling this lambda
103        // The 6 values of idx passed to the lambda are:
104        //      { 0,0 } { 0,1 } { 1,0 } { 1,1 } { 2,0 } { 2,1 }
105      }
106    );
107    // Code that executes on the host CPU (like line 91 and earlier)

Este código simple, sin una adición importante en la línea 91, no se compilará:

error C3577: Concurrency::details::_Parallel_for_each argument #3 is illegal: missing public member: 'void operator()(Concurrency::index<_Rank>) restrict(amp)'

Como se ha escrito el código, no debería haber nada que le impidiera usar en el cuerpo de la expresión lambda (líneas 92-105) algún elemento permitido por el lenguaje C++ (tal como lo admite el compilador de Visual C++). Sin embargo, existen restricciones en el uso de ciertos aspectos del lenguaje C++ en arquitecturas actuales de GPU, por lo que debe indica qué partes del código deben cumplir con estas restricciones (para así poder saber en tiempo de compilación si está infringiendo alguna regla). La indicación se debe hacer en la expresión lambda y en cualquier otra firma de función que se llame desde lambda. Por lo tanto, debe modificar la línea 91 de la siguiente manera:

91         [=](index<2> idx) restrict(amp)

Esta es una nueva característica de lenguaje de la especificación C++ AMP que se agregó al compilador Visual C++. Las funciones (incluidas las expresiones lambda) se pueden comentar con restrict(cpu), que es el valor predeterminado implícito, o con restrict(amp), tal como se muestra en el ejemplo de código anterior, o con una combinación de ambas, por ejemplo, restrict(cpu,amp). No existe otra opción. El comentario pasa a formar parte de la firma de la función, por lo que participa en la sobrecarga, lo que fue una motivación clave en el momento de diseñarla. Cuando se comenta una función con restrict(amp), se comprueba con respecto a un conjunto de restricciones y, en caso de infringir una, recibe un error de compilador. El conjunto completo de restricciones está documentado en esta publicación de blog: bit.ly/vowVlV.

Una de las restricciones restrict(amp) para las expresiones lambda es que no pueden capturar variables por referencia (consulte la advertencia que aparece casi al final de este artículo) ni tampoco pueden capturar punteros. Con esa restricción en mente, cuando mire el último listado de código para parallel_for_each, podría tener razón si se pregunta: "Si no puedo capturar por referencia y no puedo capturar punteros, ¿cómo observaré los resultados, es decir, los efectos secundarios deseables, de la expresión lambda? Cualquier cambio que haga a las variables que capture por valor no estará disponible para el código externo una vez que se complete la expresión lambda".

La respuesta a esa pregunta es un tipo que ya conoce: array_view. El objeto array_view se puede capturar por valor en la expresión lambda. Es su mecanismo para transmitir y distribuir datos. Simplemente use los objetos array_view para encapsular los contenedores reales, luego capture los objetos array_view de la expresión lambda para obtener acceso y rellenarlos y, a continuación, obtenga acceso a los objetos array_view correspondientes después de la llamada a parallel_for_each.

En resumen

Con su nuevo conocimiento, ahora puede volver a visitar la anterior adición a la matriz de CPU serie (la que utilizó array_view, extent e index) y reemplazar las líneas 15-22 de la siguiente manera:

15     parallel_for_each(e, [=](index<2> idx) restrict(amp)
16     {
19       c[idx] = a[idx] + b[idx];
22     });

Puede ver que la línea 19 sigue igual y que el bucle anidado doble con creación de objeto index manual dentro de los límites de extensión se reemplazan con una llamada a la función parallel_for_each.

Cuando se trabaja con aceleradores discretos con su propia memoria, la captura de objetos array_view en la expresión lambda transmitida a parallel_for_each resulta en una copia de los datos subyacentes en la memoria global del acelerador. De manera similar, después de la llamada de parallel_for_each, cuando obtiene acceso a los datos a través del objeto array_view (en este ejemplo, a través de c), los datos se vuelven a copiar en la memoria del host desde el acelerador.

Debe saber que si desea obtener acceso a los resultados de array_view c a través del contenedor original vC (y no través de array_view), debe llamar al método de sincronización del objeto array_view. El código debería funcionar tal como está, porque el destructor de array_view llama a la sincronización en su nombre, pero de ese modo se perdería cualquier excepción, por lo que recomiendo mejor llamar explícitamente a la sincronización. Por esto, agregue una instrucción en cualquier lugar después de la llamada de parallel_for_each, de esta manera:

23          c.synchronize();

La reversión (que garantiza que array_view tiene los datos más reciente de su contenedor original, si este ha cambiado) se logra gracias al método de actualización.

Y lo que es más importante, copiar datos en todo (normalmente) un bus PCIe puede resultar muy caro, por lo que solo desea copiar en la dirección necesaria. En el listado anterior, se podían modificar las líneas 11-13 para indicar que los datos subyacentes de los objetos array_view a y b se deben copiar al acelerador (pero no se copiarán de vuelta) y también que no es necesario copiar al acelerador los datos subyacentes de array_view c. En el siguiente fragmento de código, los cambios necesarios aparecen en negrita:

11          array_view<const int, 2> a(e, vA), b(e, vB);
12          array_view<int, 2> c(e, vC);
13          c.discard_data();

Sin embargo, incluso con estas modificaciones aplicadas, el algoritmo de adición de la matriz no utiliza los cálculos suficientes como para compensar la sobrecarga de la copia de datos, por lo que no resulta ser un buen candidato para la paralelización con C++ AMP. Solo lo he usado para enseñarle los conceptos básicos.

Dicho eso, y al usar este ejemplo simple en todo el artículo, ahora tiene las habilidades para paralelizar otros algoritmos que tengan los cálculos suficientes como para generar beneficios. Un algoritmo de ese tipo es la multiplicación de matriz. Sin ningún comentario de mi parte, asegúrese de comprender esta simple implementación serie del algoritmo de multiplicación de matriz:

void MatMul(vector<int>& vC, const vector<int>& vA,
  const vector<int>& vB, int M, int N, int W)
{
  for (int row = 0; row < M; row++)
  {
    for (int col = 0; col < N; col++)
    {
      int sum = 0;
      for(int i = 0; i < W; i++)
        sum += vA[row * W + i] * vB[i * N + col];
      vC[row * N + col] = sum;
    }
  }
}

… y la implementación correspondiente de C++ AMP:

array_view<const int, 2> a(M, W, vA), b(W, N, vB);
array_view<int, 2> c(M, N, vC);
c.discard_data();
parallel_for_each(c.extent, [=](index<2> idx) restrict(amp)
{
  int row = idx[0]; int col = idx[1];
  int sum = 0;
  for(int i = 0; i < b.extent[0]; i++)
    sum += a(row, i) * b(i, col);
  c[idx] = sum;
});
c.synchronize();

En mi equipo portátil, la multiplicación de matriz de C++ AMP genera una mejora en el rendimiento de más de 40 veces en comparación con el código de CPU serie para M=N=W=1024.

Ahora que entiende todos los conceptos básicos, es posible que se pregunte cómo puede elegir el acelerador en el cual ejecutar su algoritmo, una vez que lo implemente usando C++ AMP. Examinemos eso a continuación.

accelerator y accelerator_view

Parte del espacio de nombres de simultaneidad es el nuevo tipo de acelerador. Representa un dispositivo en el sistema que el tiempo de ejecución de C++ AMP puede usar, que para la primera versión es hardware con un instalador DirectX 11 instalado (o emuladores de DirectX).

Cuando se inicia el tiempo de ejecución de C++ AMP, enumera todos los aceleradores y, según la heurística interna, elige uno como el valor predeterminado. Esa es la razón por la que no ha tenido que tratar directamente con los aceleradores en todo el código anterior: se eligió uno de manera predeterminada. Si desea enumerar los aceleradores e incluso elegir el predeterminado, puede hacerlo de manera muy simple, tal como se aprecia en el código que se explica por sí mismo que aparece en la figura 3.

Figura 3 Elección de un acelerador

26 accelerator pick_accelerator()
27 {
28   // Get all accelerators known to the C++ AMP runtime
29   vector<accelerator> accs = accelerator::get_all();
30
31   // Empty ctor returns the one picked by the runtime by default
32   accelerator chosen_one;
33
34   // Choose one; one that isn't emulated, for example
35   auto result =
36     std::find_if(accs.begin(), accs.end(), [] (accelerator acc)
37   {
38     return !acc.is_emulated; //.supports_double_precision
39   });
40   if (result != accs.end())
41     chosen_one = *(result); // else not shown
42
43   // Output its description (tip: explore the other properties)
44   std::wcout << chosen_one.description << std::endl;
45
46   // Set it as default ... can only call this once per process
47   accelerator::set_default(chosen_one.device_path);
48
49   // ... or just return it
50   return chosen_one;
51 }

En la línea 38 puede ver consultas de una de las muchas propiedades de acelerador y otras aparecen en la figura 4.

accelerator and accelerator_view Classes
Figura 4 Clases accelerator y accelerator_view

Si desea tener llamadas de parallel_for_each diferentes que usen aceleradores distintos, o si por cualquier otra razón desea ser más explícito que definir el acelerador predeterminado globalmente para un proceso, necesita transmitir un objeto accelerator_view a parallel_for_each. Esto es posible porque parallel_for_each tiene una sobrecarga que acepta un accelerator_view como el primer parámetro. Obtener un objeto accelerator_view es tan fácil como llamar a default_view en un objeto accelerator; por ejemplo:

accelerator_view acc_vw = pick_accelerator().default_view;

Más allá del hardware de DirectX 11, hay tres aceleradores especiales que C++ AMP pone a disposición:

  • direct3d_ref: útil para la depuración de corrección, pero no es útil en producción dado que es mucho más lento que cualquier hardware real.
  • direct3d_warp: una solución de reserva para ejecutar su código de C++ AMP en la CPU usando multinúcleo y extensiones SIMB de transmisión hoy.
  • cpu_accelerator: no tiene la capacidad de ejecutar nada de código C++ AMP en esta versión. Solo es útil para configurar matrices de almacenamiento provisional (una técnica de optimización avanzada), lo que va más allá del alcance de este artículo pero se describe en esta publicación de blog: bit.ly/vRksnn.

Mosaico y lectura posterior por parte del usuario

El tema más importante que no abarca este artículo es el mosaico.

Desde una perspectiva de escenario, el mosaico toma las enormes mejoras en el rendimiento que ve con las técnicas de codificación exploradas hasta ahora y (potencialmente) incluso aumenta esas ganancias. Desde una perspectiva de API, el mosaico consiste en tipos tiled_index y tiled_extent, así como también un tipo tile_barrier y una clase de almacenamiento tile_static. También hay una sobrecarga en parallel_for_each que acepta un objeto tiled_extent con una expresión lambda que acepta un objeto tiled_index. Dentro de esa expresión lambda, puede usar objetos tile_barrier y variables tile_static. Hablo sobre el mosaico en mi segundo artículo sobre C++ AMP en la página 40.

Hay otros temas que puede explorar con la ayuda de publicaciones en blogs y con la documentación en línea de MSDN:

  • <amp_math.h> es una biblioteca matemática con dos espacios de nombres, uno para funciones matemáticas de alta precisión y otro para funciones matemáticas rápidas pero menos precisas. Decida según sus capacidades de hardware y según los requisitos de su escenario.
  • <amp_graphics.h> y <amp_short_vectors.h> más algunas funciones de la interoperabilidad de DirectX se encuentran disponibles para trabajar con la programación de gráficos.
  • concurrency::array es un tipo de datos de contenedor enlazado a un acelerador, con una interfaz casi idéntica a array_view. Este tipo es uno de los dos (el otro es la textura en el espacio de nombres de gráficos) que se deben capturar por referencia en la expresión lambda transmitida a parallel_for_each. Esta es la advertencia a la que me referí anteriormente.
  • Compatibilidad con objetos intrínsecos de DirectX, como objetos atómicos para sincronización entre subprocesos.
  • Depuración y generación de perfiles de GPU en Visual Studio 11.

Protección de sus inversiones contra la obsolescencia

En este artículo le presenté una moderna API paralela de datos de C++ que le permite expresar sus algoritmos de manera que sus aplicación puede usar GPU para aumentar el rendimiento. C++ AMP está diseñado para que pueda proteger contra la obsolescencia sus inversiones en hardware que todavía está por conocer.

Aprendió cómo unos cuantos tipos (array_view, extent e index) le ayudan a trabajar con datos multidimensionales, combinados con una función global única (parallel_for_each) que le permite ejecutar su código, partiendo por una expresión lambda restrict(amp), en un acelerador (que puede especificar a través de los objetos accelerator y accelerator_view).

Más allá de la implementación de Microsoft Visual C++, C++ AMP se brinda a la comunidad como una especificación abierta que cualquier persona puede implementar en cualquier plataforma.

Daniel Moth es administrador principal de programas en la División de desarrolladores de Microsoft. Puede ponerse en contacto con él a través de su blog en danielmoth.com/Blog.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Steve Deitz, Yossi Levanoni, Robin Reynolds-Haertle, Stephen Toub y Weirong Zhu