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

ein*_*ica 9 c++ c++-faq std-span mdspan

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

请解释一下这个神秘实体的用途,以及我何时需要使用它。

ein*_*ica 15

TL;DRmdspan是多维度的扩展std::span- 在内存布局和访问模式方面具有大量(不可避免的)灵活可配置性。


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

“它是什么?” (简单版)

一个mdspan<T>是:

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

mdspan-解释布局的插图

如果我们有:

std::vector v = {1,2,3,4,5,6,7,8,9,10,11,12};
Run Code Online (Sandbox Code Playgroud)

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

auto sp1 = std::span(v.data(), 12);
auto mdsp1 = std::mdspan(v.data(), 12);
Run Code Online (Sandbox Code Playgroud)

或范围 2 x 6 的二维数组:

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

或 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 ) )
Run Code Online (Sandbox Code Playgroud)

我们也可以将其视为 3 x 2 x 2 或 2 x 2 x 3 数组,或 3 x 4 等等。

“我应该什么时候使用它?”

  • operator[](C++23 及更高版本)当您想在从某处获得的某个缓冲区上使用多维时。因此,在上面的例子中,ms3[1, 2, 0]is11ms3[0, 1, 1]is 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]);
    }
    
    Run Code Online (Sandbox Code Playgroud)

    你可以写:

    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]);
    }
    
    Run Code Online (Sandbox Code Playgroud)
  • 作为传递多维 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);
    }
    
    Run Code Online (Sandbox Code Playgroud)

标准化状况

虽然std::span在 C++20 中已标准化,但std::mdspan事实并非如此。然而,它是 C++23 的一部分,几乎已经完成(等待最终投票)。

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

“这些‘额外功能’提供了什么mdspan?”

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

template <
    class T,
    class Extents,
    class LayoutPolicy = layout_right,
    class AccessorPolicy = default_accessor<ElementType>
>
class mdspan;
Run Code Online (Sandbox Code Playgroud)

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

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

    auto my_extents extents<dynamic_extent, 3, dynamic_extent>{ 2, 4 };
    
    Run Code Online (Sandbox Code Playgroud)

    ... 是与 对应的范围对象,但仅存储类实例中的dextents<size_t>{ 2, 3, 4 }2和;编译器知道只要使用第二个维度4就需要插入。3

  • 您可以使用 Fortran 风格将维度从小到大,而不是像 C 中那样从大到小。因此,如果您设置LayoutPolicy = layout_left,则mds[x,y]是 atmds.data[mds.extent(0) * y + x]而不是通常的mds.data[mds.extent(1) * x + y]

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

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

  • 您可以在每个维度上使用偏移量“切割”您的mdspan(例如,采用矩阵的子矩阵) - 结果仍然是mdspan!...那是因为你可以有一个包含这些偏移量的mdspana LayoutPolicy。此功能在 C++23 IIANM 中不可用。

  • 使用AccessorPolicy,您可以使mdspan's 真正拥有它们所引用的数据,无论是单独的还是集体的。

进一步阅读

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