Marius Bancila
Marius Bancila

Reputation: 16338

How to use std::mdspan with the std::layout_stride policy

I'd like to understand how the std::layout_stride policy for the std::mdspan works. At this point, no compiler supports this new C++23 library type, although a reference implementation exists: https://github.com/kokkos/mdspan. However, I could not find a good explanation on this layout type either on the github wiki (A Gentle Introduction to mdspan) or the P0009r18 paper.

The following program, using std::layout_right (the default) for an mdspan prints

1 2 3
4 5 6
std::vector v {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30};
std::mdspan<int, 
            std::extents<size_t, 2, 3>,
            std::layout_right> mv{v.data()};
for (std::size_t r = 0; r < mv.extent(0); r++)
{
  for (std::size_t c = 0; c < mv.extent(1); c++)
  {
     std::cout << mv[r, c] << ' ';
  }
  std::cout << '\n';
}

If we change to std::layout_left, the output becomes:

1 3 5
2 4 6
std::mdspan<int, 
            std::extents<size_t, 2, 3>,
            std::layout_left> mv{v.data()};

My understanding is that std::layout_stride can control the stride. For instance, jumping every 2 elements (from the underlying sequence) for rows and 3 elements for columns. However, I did not find any example on this matter. Does anyone have examples showing how this really works?

It can be experimented on godbolt here: https://godbolt.org/z/Mxa7cej1a.

UPDATE

Based on the answer from @KamilCuc, I deduce the following:

    stdex::mdspan<int, 
                  stdex::extents<size_t, stdex::dynamic_extent, stdex::dynamic_extent>, 
                  stdex::layout_stride> 
    mv{ v.data(), 
       { stdex::dextents<size_t,2>{2, 3}, 
         std::array<std::size_t, 2>{3, 1}}};

result:

1 2 3
4 5 6

This is the equivalent of layout_right.

stride: std::array<std::size_t, 2>{1, 1}

1 2 3
2 3 4

stride: std::array<std::size_t, 2>{3, 2}

1 3 5
4 6 8

stride: std::array<std::size_t, 2>{9, 3}

 1  4  7
10 13 16

Upvotes: 8

Views: 1057

Answers (2)

Mark Hoemmen
Mark Hoemmen

Reputation: 41

An mdspan's layout mapping maps from a rank()-dimensional index, to a 1-D "offset" into that underlying 1-D array. Stride k of a strided layout mapping expresses how many elements to skip in the underlying 1-D array, if you increase the index for extent k by 1.

For example, a 2 x 3 layout_right mdspan has strides of (3, 1). That is, if you increase the row index by 1, the offset is increased by 3 (skip 3 elements in the underlying 1-D array). If you increase the column index by 1, the offset is increase by 1 (go to the next element in the underlying 1-D array).

For instance, jumping every 2 elements (from the underlying sequence) for rows and 3 elements for columns.

Do you mean strides (2, 3), like the following?

1 4 7
3 6 9

In general, this would result in a nonunique layout. Here is a 4 x 5 example. Note that some values (like 7, 10, and 13) repeat. For example, index tuple (0,2) maps to offset 6, and index tuple (3,0) also maps to offset 6. (The offset is the dot product of the indices and the strides.)

1  4  7 10 13
3  6  9 12 15
5  8 11 14 17 
7 10 13 16 19

Here is an example that illustrates a rank-2 mdspan that views every other element of an underlying 1-D array.

https://clang.godbolt.org/z/h543f6zYv

Here is the source code from that example, for future reference.

#include <mdspan>
#include <print>
#include <ranges>
#include <vector>

// Temporary work-around until C++26 feature std::dims (P2389R2) is implemented.
namespace std {
  template<size_t Rank, class IndexType = size_t>
  using dims = dextents<IndexType, Rank>;
} // namespace std

template<class ElementType,
  class IndexType, size_t Extent0, size_t Extent1,
  class Layout,
  class Accessor>
void print_rank2_mdspan(
    std::mdspan<
      ElementType,
      std::extents<IndexType, Extent0, Extent1>,
      Layout, Accessor> m)
{
    std::println("[");
    for (IndexType row = 0; row < m.extent(0); ++row) {
        std::print("  [");
        for (IndexType col = 0; col < m.extent(1); ++col) {
            std::print("{}", m[row, col]);
            if (col + IndexType(1) < m.extent(1)) {
                std::print(", ");
            }
        }
        std::print("]{}\n", (row + IndexType(1) < m.extent(0) ? "," : ""));
    }
    std::println("]");
}

int main() {
    std::vector<int> data(std::from_range, std::views::iota(0, 32));
    std::println("Original elements: {:}", data);

    // Example: 1D mdspan with stride of 2:
    {
        size_t stride = 2;
        size_t num_elements = 16;
        std::dims<1> extents(num_elements);
        auto mapping = std::layout_stride::mapping(
            extents, std::array{stride});
        // Create mdspan:
        std::mdspan m{data.data(), mapping};

        std::print("rank-1 mdspan: [");
        for (size_t i = 0; i < m.extent(0); ++i) {
            std::print("{}", m[i]);
            if (i + 1 < m.extent(0)) {
                std::print(", ");
            }
        }
        std::println("]");
    }

    // Example: 2D mdspan viewing the same elements
    // as the 1D mdspan above, in column-major order.
    {
        size_t stride_x = 2;
        // stride_y is "how many elements we need to skip in order to
        // get to the element one to the right in the 2D array."
        // It can't be less than nelem_x,
        // else the layout mapping wouldn't be unique.
        size_t stride_y = 8;
        size_t nelem_x = 4;
        size_t nelem_y = 4;
        std::dims<2> extents(nelem_x, nelem_y);
        auto mapping = std::layout_stride::mapping(
            extents, std::array{stride_x, stride_y});
        std::mdspan m{data.data(), mapping};
        std::println("rank-2 column-major mdspan:");
        print_rank2_mdspan(m);
    }

    // Example: 2D mdspan viewing the same elements
    // as the 1D mdspan above, in row-major order.
    {
        size_t stride_x = 8, stride_y = 2;
        size_t nelem_x = 4;
        size_t nelem_y = 4;
        std::dims<2> extents(nelem_x, nelem_y);
        auto mapping = std::layout_stride::mapping(
            extents, std::array{stride_x, stride_y});
        std::mdspan m{data.data(), mapping};
        std::println("rank-2 row-major mdspan:");
        print_rank2_mdspan(m);
    }

    return 0;
}

Upvotes: 4

paleonix
paleonix

Reputation: 3095

Answer inspired by OP's update which in turn was inspired by @KamilCuc's comment:

Using CTAD* one can nicely create an mdspan with layout_stride like this:

std::dextents<std::size_t, 2> shape{2, 3};
std::array<std::size_t, 2> strides{shape.extent(1), 1};

std::mdspan mv{v.data(), std::layout_stride::mapping{shape, strides}};

These particular strides just result in the same mapping as layout_right. I refer back to OP's update for further examples of different strides.

CTAD will cause mdspan to infer the data type from v.data(), and both the extents (including index type) and the layout from the mapping passed as the second argument to the constructor. In this example CTAD is also used to deduce the extents template argument of layout_stride::mapping. One could go even further with CTAD like here:

// using C++23 size_t literals
std::extents shape{2uz, 3uz};
std::array strides{shape.extent(1), 1uz};

Note that one cannot use dextents with CTAD which makes sense as they are there as a shortcut when not using CTAD and writing dextents with CTAD would actually be longer than writing extents. The resulting extents type is the same.


*Class Template Argument Deduction

Upvotes: 4

Related Questions