什么是 mdspan,它的用途是什么?

What is an mdspan, and what is it used for?

提问人:einpoklum 提问时间:3/19/2023 最后编辑:Jan Schultkeeinpoklum 更新时间:11/16/2023 访问量:1367

问:

在过去一年左右的时间里,我注意到 StackOverflow 上一些与 C++ 相关的答案指的是 - 但我从未在 C++ 代码中真正看到过这些答案。我尝试在 C++ 编译器的标准库目录和 C++ 编码指南中查找它们 - 但找不到它们。我确实找到了;我猜它们是相关的——但是怎么回事?这个添加的“md”代表什么?mdspanstd::span

请解释一下这个神秘实体是关于什么的,以及我什么时候可能想使用它。

C C++-FAQ 标准跨度 mdspan

评论


答:

12赞 einpoklum 3/19/2023 #1

TL;DR:是多维度的扩展 - 具有许多(不可避免的)灵活的可配置性,以及内存布局和访问模式。mdspanstd::span


在阅读此答案之前,您应该确保您清楚什么是跨度以及它的用途。现在已经不碍事了:由于 可能是相当复杂的野兽(通常 ~7 倍或更多的源代码作为实现),我们将从简化的描述开始,并保留高级功能在下面进一步介绍。mdspanstd::span

“这是什么?”(简易版)

一个是:mdspan<T>

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

-interpreted 布局的图示mdspan

如果我们有:

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

我们可以将 的数据视为包含 12 个元素的一维数组,类似于其原始定义:v

auto sp1 = std::span(v.data(), 12);
auto mdsp1 = std::mdspan(v.data(), 12);

或范围为 2 x 6 的 2D 数组:

auto mdsp2 = std::mdspan(v.data(), 2, 6 );
// (  1,  2,  3,  4,  5,  6 ),
// (  7,  8,  9, 10, 11, 12 )

或 2 x 3 x 2 的 3D 阵列:

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]11ms3[0, 1, 1]4

  • 当您想要传递多维数据而不分离原始数据指针和维度时。你在内存中有一堆元素,并希望使用多个维度来引用它们。因此,而不是:

    void print_matrix_element(
       float const* matrix, size_t row_width, size_t x, size_t y) 
    {
       std::print("{}", matrix[row_width * x + y]);
    }
    

    你可以这样写:

    void print_matrix_element(
        std::mdspan<float const, std::dextents<size_t, 2>> matrix,
        size_t x, size_t y)
    {
       std::print("{}", matrix[x, y]);
    }
    
  • 作为传递多维 C 数组的正确类型:
    C 完美地支持多维数组......只要它们的维度是在编译时给出的,并且你不要尝试将它们传递给函数。这样做有点棘手,因为最外层的维度会衰减,所以你实际上会传递一个指针。但是使用 mdspans,您可以这样写:

    template <typename T, typename Extents>
    void print_3d_array(std::mdspan<T, Extents> ms3)
    {
       static_assert(ms3.rank() == 3, "Unsupported rank");
       // read back using 3D view
       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);
          // Note: This construction can probably be improved, it's kind of fugly
    
        print_3d_array(ms3);
    }
    

标准化现状

虽然在 C++20 中是标准化的,但不是。但是,它是 C++23 的一部分,它几乎已经完成(等待最终投票)。std::spanstd::mdspan

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

“这些'额外功能'提供了哪些?”mdspan

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

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

这个答案已经很长了,所以我们不会给出完整的细节,但是:

  • 某些盘区可以是“静态”而不是“动态”,在编译时指定,因此不存储在实例数据成员中。仅存储“动态”实例。例如,这个:

    auto my_extents extents<dynamic_extent, 3, dynamic_extent>{ 2, 4 };
    

    ...是对应于 的扩展对象,但它只在类实例中存储值 和 ;编译器知道每当使用第二个维度时都需要插入。dextents<size_t>{ 2, 3, 4 }243

  • 您可以让维度以 Fortran 样式从次要到主要,而不是像 C 那样从主要到次要。因此,如果将 设置为 ,则 是 at 而不是通常的 。LayoutPolicy = layout_leftmds[x,y]mds.data[mds.extent(0) * y + x]mds.data[mds.extent(1) * x + y]

  • 您可以将您的“重塑”为另一个具有不同尺寸但整体尺寸相同的人。mdspanmdspan

  • 您可以使用“strides”定义布局策略:将 mdspan 中的连续元素在内存中保持固定距离;具有额外的偏移量以及每行或维度切片的开头和/或结尾;等。

  • 你可以用每个维度的偏移量来“切割”你的 (例如,取矩阵的子矩阵) - 结果仍然是 !...这是因为你可以有一个包含这些偏移量的 A 和 a。此功能在 C++23 IIANM 中不可用。mdspanmdspanmdspanLayoutPolicy

  • 使用 ,您可以单独或集体地使 实际上拥有它们所引用的数据。AccessorPolicymdspan

延伸阅读

(一些例子改编自这些来源。