La propuesta de estandarización P0009 para C++23 [1], cuya implementación puede encontrarse en la referencia [2], permite la adecuada gestión en C++ moderno de vistas no-propietarias y potencialmente mutables de arrays multidimensionales. Estas vistas son una herramienta de enorme interés en campos tan diversos como la Física, la Matemática o la Ingeniería y constituyen la piedra angular en torno a la cual el comité ISO C++ pretende diseñar una biblioteca de álgebra lineal basada en BLAS [3].
Sea I un espacio de índices multidimensionales de rango R, definido como el producto cartesiano de intervalos semiabiertos [0, N0) ⨯ [0, N1) ⨯ ... ⨯ [0, NR-1), con Nk natural para cada k = 0, ..., R-1. Una vista no propietaria de un array multidimensional de extensión k-ésima igual a Nk asociará a cada R-tupla de índices de acceso i ∈ I una referencia a un elemento accesible a través de un rango contiguo de índices enteros.
En el caso que nos ocupa, tales vistas se obtendrán a través de la plantilla de clase std::experimental::mdspan, que cuenta con los siguientes cuatro parámetros:
- Element_type: el tipo de dato referenciado.
- Extents: una especialización de la plantilla de clase variádica std::experimental::extents, cuyo número de parámetros coincide con el número de extensiones (rango R) del array multidimensional, permitiendo especificar sus longitudes Nk de forma estática o dinámica (véanse los ejemplos más abajo).
- Layout_policy (opcional): especifica la fórmula (y las propiedades de la fórmula) que mapea un índice multidimensional i ∈ I a un elemento del array. La biblioteca emplea por defecto el orden de fila-principal propio de C y C++ (layout_right, row-major), si bien proporciona también el orden de columna-principal característico de Fortran y MATLAB (layout_left, column-major), así como una generalización de los órdenes anteriores que permite registrar un paso diferente (potencialmente distinto a la unidad) para cada extensión (layout_stride).
- Accessor_policy (opcional): el descriptor de acceso que gobierna cómo se leen y se escriben los elementos (por ejemplo, de forma atómica).
A modo de ejemplo, consideremos la matriz
con R = 2 y extensiones N0 = 3 y N1 = 4. Adoptando un orden de fila-principal, podríamos almacenar las entradas numéricas de la matriz de forma contigua en memoria mediante un contenedor de tipo std::array<int,12>:
Resaltemos la elevada localidad espacial de las entradas así conseguida. Resulta inmediato generar una vista a un array 2-dimensional 3⨯4 a través de data. Pudiendo optar por establecer las extensiones N0 y N1 en tiempo de compilación, codificaríamos:
siendo ms de tipo stde::mdspan<int,stde::extents<3ull,4ull>,stde::layout_right, stde::default_accessor<int>>. La figura inferior proporciona un esquema tanto de los datos almacenados en memoria virtual como de la vista para ellos obtenida:
Resaltemos la separación estricta entre el espacio de almacenamiento de los datos y la vista mdspan, esta última no-propietaria.
En el ejemplo considerado, las llamadas a las funciones miembro públicas ms.rank(), ms.extent(0) y ms.extent(1) devolverán, respectivamente, el rango R = 2 y las extensiones N0 = 3 (número de filas) y N1 = 4 (número de columnas) del array multidimensional. La matriz podría ser entonces representada en la terminal mediante el bucle siguiente:
donde hemos empleado rangos-factoría std::views::iota para generar secuencias de índices 1-dimensionales para cada extensión (todas ellas empiezan en cero), así como el sufijo uz propio de C++23 para inicializar valores de tipo std::size_t. Si deseáramos, por ejemplo, multiplicar por 2 todos los elementos de la segunda fila de la matriz, codificaríamos simplemente:
Podemos también obtener una vista para un subconjunto de un objeto mdspan ya existente. Ello es posible gracias a la función de slicing submdspan:
Especificador de slice | Argumento de submdspan | Reduce el rango |
---|---|---|
Único índice | Entero | Sí |
Rango de índices | std::pair o std::tuple con dos enteros | No |
Todos los índices | std::experimental::full_extent | No |
Una de las principales características de la propuesta P0009 consiste en la posibilidad de emplear, e incluso combinar, tanto extensiones dinámicas como estáticas. Siguiendo el ejemplo proporcionado en el seminario [4] impartido por Bryce Adelstein Lelbach (Nvidia), consideremos el cálculo paralelizado de la matriz transpuesta de una matriz A cuyas extensiones m⨯n sean establecidas en runtime. A efectos puramente ilustrativos, rellenaremos las entradas de la matriz con valores reales pseudo-aleatorios:
donde la vista generada es de tipo stde::mdspan<double,stde::dextents<2>, stde::layout_right,stde::default_accessor<double>>, siendo stde::dextents<2> un mero alias para stde::extents<stde::dynamic_extent, stde::dynamic_extent>. La matriz transpuesta B = At se calcularía entonces como:
donde hemos empleado la implementación de la vista de producto cartesiano proporcionada por la biblioteca de rangos [5] con el fin de definir el espacio de índices multidimensionales de la matriz B.
Observemos por último que las vistas mdspan pueden ser capturadas por valor sin incurrir apenas en coste de espacio (especialmente cuando sus extensiones sean establecidas estáticamente), dado que éstas implementan una semántica de referencia.
- Programming Language C++ LEWG - P0009r14 - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0009r14.html
- Reference mdspan implementation - https://github.com/kokkos/mdspan/
- D1673R4: A free function linear algebra interface based on the BLAS - http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p1673r4.html
- C++ Standard Parallelism - Bryce Adelstein Lelbach - CppCon 2021 - https://youtu.be/LW_T2RGXego
- tl libraries - Ranges - https://github.com/TartanLlama/ranges
No hay comentarios:
Publicar un comentario