Reputation: 16338
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
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
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