什么是mdspan,它用于什么?

7
在过去的一年左右,我注意到StackOverflow上有一些与C++相关的答案提到了`mdspan`,但实际上我从未在C++代码中见过这些。我尝试在我的C++编译器的标准库目录和C++编码指南中寻找它们,但没有找到。我倒是找到了`std::span`,我猜它们可能是相关的,但具体是什么意思呢?而这个"md"代表什么?
请解释一下这个神秘的实体是什么,以及何时我可能想要使用它。
1个回答

12
TL;DR: mdspanstd::span的多维扩展,具有许多(不可避免的)灵活配置选项,可用于内存布局和访问模式。
在阅读本答案之前,你应该确保你清楚什么是标签以及它的用途。既然这一点已经解决了:由于mdspan可能是相当复杂的东西(通常比std::span实现的源代码多大约7倍或更多),我们将从一个简化的描述开始,将高级功能保留在下面进一步讨论。

"它是什么?"(简化版本)

mdspan<T>是:

字面上,"multi-dimensional span"(类型为T的元素)。
从一维/线性元素序列到多维的std::span<T>的泛化。
一种非拥有的视图,用于解释内存中类型为T的连续元素序列,被解释为多维数组。
基本上只是一个struct { T * ptr; size_type extents[d]; },带有一些方便的方法(用于在运行时确定的d维度)。

mdspan解释布局的示例

如果我们有:

std::vector v = {1,2,3,4,5,6,7,8,9,10,11,12};

我们可以将v的数据视为一个由12个元素组成的一维数组,类似于它的原始定义。
auto sp1 = std::span(v.data(), 12);
auto mdsp1 = std::mdspan(v.data(), 12);

或者一个2 x 6的二维数组:
auto mdsp2 = std::mdspan(v.data(), 2, 6 );
// (  1,  2,  3,  4,  5,  6 ),
// (  7,  8,  9, 10, 11, 12 )

或者一个3D数组2 x 3 x 2:
auto ms3 = std::mdspan(v.data(), 2, 3, 2);
// ( ( 1,  2 ), ( 3,  4 ), (  5,  6 ) ),
// ( ( 7,  8 ), ( 9, 10 ), ( 11, 12 ) )

我们还可以将其视为3 x 2 x 2或2 x 2 x 3的数组,或者3 x 4等等。
"我应该在什么时候使用它?"
(C++23及更高版本)当您想在从某处获取的缓冲区上使用多维`operator[]`时。因此,在上面的示例中,`ms3[1, 2, 0]`是`11`,`ms3[0, 1, 1]`是`4`。
当您想要传递多维数据而不需要分离原始数据指针和维度时。您已经在内存中获取了一堆元素,并希望使用多个维度引用它们。因此,不再需要:
```cpp void print_matrix_element( float const* matrix, size_t row_width, size_t x, size_t y) { std::print("{}", matrix[row_width * x + y]); } ```
您可以这样编写:
```cpp void print_matrix_element( std::mdspan> matrix, size_t x, size_t y) { std::print("{}", matrix[x, y]); } ```
作为传递多维C数组的正确类型:C完美支持多维数组...只要它们的维度在编译时给定,并且您不尝试将它们传递给函数。这样做有点棘手,因为最外层的维度会发生衰减,所以实际上您将传递一个指针。但是使用mdspans,您可以这样编写:
```cpp template void print_3d_array(std::mdspan ms3) { static_assert(ms3.rank() == 3, "Unsupported rank"); // 使用3D视图读取 for(size_t i=0; i != ms3.extent(0); i++) { fmt::print("slice @ i = {}\n", i); for(size_t j=0; j != ms3.extent(1); j++) { for(size_t k=0; k != ms3.extent(2); k++) fmt::print("{} ", ms3[i, j, k]); fmt::print("\n"); } } }
int main() { int arr[2][3][2] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
auto ms3 = std::mdspan(&arr[0][0][0], 2, 3, 2); // 注意:这个构造可能可以改进,有点丑陋
print_3d_array(ms3); } ```

标准化状态

虽然 std::span 在 C++20 中已经标准化,但 std::mdspan 还没有。然而,它是 C++23 的一部分,该版本已经接近最终定稿(等待最终投票)。

您已经可以使用一个参考实现。它是美国 Sandia 国家实验室的"Kokkos性能可移植生态系统"的一部分。

mdspan 提供了哪些‘额外功能’?”

mdspan 实际上有4个模板参数,不仅仅是元素类型和范围:

template <
    class T,
    class Extents,
    class LayoutPolicy = layout_right,
    class AccessorPolicy = default_accessor<ElementType>
>
class mdspan;

这个答案已经相当长了,所以我们不会提供完整的细节,但是:
一些范围可以是“静态”的,而不是“动态”的,可以在编译时指定,因此不会存储在实例数据成员中。只有“动态”的实例会被存储。例如,这个例子中的范围对象对应于dextents<size_t>{ 2, 3, 4 },但只在类实例中存储值24;编译器知道在使用第二个维度时需要插入3
您可以按照Fortran风格,从次要到主要的顺序设置维度,而不是像C语言那样从主要到次要的顺序。因此,如果您设置LayoutPolicy = layout_left,那么mds[x,y]将位于mds.data[mds.extent(0) * y + x],而不是通常的mds.data[mds.extent(1) * x + y]
您可以将您的mdspan重新调整为具有不同维度但相同总大小的另一个mdspan
您可以使用“步长”定义布局策略:在mdspan中连续的元素在内存中具有固定的距离;在每行或每个维度切片的开头和/或结尾具有额外的偏移量等。
您可以在每个维度上使用偏移量“切割”您的mdspan(例如,从矩阵中取子矩阵)-结果仍然是一个mdspan!这是因为您可以使用包含这些偏移量的LayoutPolicy来创建mdspan。这个功能在C++23中不可用。
使用AccessorPolicy,您可以使mdspan实际上拥有它们所引用的数据,无论是个别还是集体。

进一步阅读

(部分示例参考自这些来源。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接