yohhoyの日記

技術的メモをしていきたい日記

std::mdspan

C++2b(C++23)標準ライブラリに追加される多次元ビューstd::mdspanについて。multidimensional spanの略。

// <mdspan>ヘッダ
namespace std {
  template<
    class ElementType,
    class Extents,
    class LayoutPolicy = layout_right,
    class AccessorPolicy = default_accessor<ElementType>>
  class mdspan;
}

まとめ:

  • メモリ領域に対して多次元配列風の要素アクセスビューを提供する。
    • 参照先メモリ領域の所有権を管理しない。*1
    • C++20 std::spanの多次元拡張版。
  • mdspanコンストラクタと推論ガイド(deduction guide)が多数提供される(→id:yohhoy:20230308
  • mdspanのコピーは定数時間。
    • 多次元ビューサイズがコンパイル時定数ならポインタ1個分、実行時変数なら ポインタ1個+インデクス値×次元数(rank) 程度のメモリコピー。
  • 要素アクセスには多次元[]演算子を利用。*2
    • 内部的に多次元インデクスからメモリ領域へのオフセット位置へ換算する。
    • 例:m[1, 2], data[i, j, k]
  • Extents:多次元ビューのインデクス型I*3、次元数(rank)、各次元の要素数(extent)。
    • インデクス型Iを明示的に指定する(通常はsize_t型)。*4
    • 次元数はコンパイル時定数として与える。
    • std::extents<I, E1, ... En>:次元数N。各次元の要素数コンパイル時または実行時(std::dynamic_extent)に決定する。両者は混在可能。*5
    • std::dextents<I, R>:次元数R。全次元の要素数が実行時に決定する。
  • LayoutPolicy:多次元ビューのメモリレイアウトをカスタマイズ可能。
    • layout_right:行優先(row major)レイアウト。C/C++互換。
    • layout_left:列優先(column major)レイアウト。FortranMATLAB互換。
    • ユーザ定義クラスによる高度なカスタマイズもサポートする(→id:yohhoy:20230315)。*6
  • AccessorPolicy:要素アクセス方法をカスタマイズ可能。
    • 既定動作は通常のメモリアクセスを行う。メモリ領域ポインタptrに対してptr[index]
    • Atomicな要素アクセス(std::atomic_ref)や、ヘテロジニアスメモリ環境(GPUなど)を想定したカスタマイズポイント。*7
  • C++2b(C++23)時点ではmdspanの “部分ビュー/Slicing” 表現は存在しない。
    • 次期C++2c(C++26)にてmdspanから部分ビューを取り出すsubmdspan関数が追加される(→id:yohhoy:20240201)。
  • 1次元ビューstd::spanとは異なり、範囲for文による要素走査はできない。
    • extent(rank)メンバ関数で各次元の要素数を取得し、多次元インデクスによる要素アクセスを行う。

基本的な利用例

// C++2b(C++23)
#include <array>
#include <mdspan>
#include <print>

int a[] = { 1, 2, 3, 4, 5, 6 };

// 2x3要素の2次元ビュー/動的サイズ
int cols = 2, rows = 3;
std::mdspan m1{a, cols, rows};
for (size_t i = 0; i < m1.extent(0); ++i)
  for (size_t j = 0; j < m1.extent(1); ++j)
    std::println("m1[{},{}]=={}", i, j, m1[i, j]);
// m1[0,0]==1  m1[0,1]==2  m1[0,2]==3
// m1[1,0]==4  m1[1,1]==5  m1[1,2]==6

// 3x2要素の2次元ビュー/固定サイズ
using Mat3x2 = std::mdspan<int, std::extents<size_t, 3, 2>>;
Mat3x2 m2{a};
// m2[0,0]==1  m2[0,1]==2
// m2[1,0]==3  m2[1,1]==4
// m2[2,0]==5  m2[2,1]==6

// 列優先3x2要素の2次元ビュー
using FortranMatrix =
  std::mdspan<int, std::dextents<size_t, 2>, std::layout_left>;
FortranMatrix m3{a, 3, 2};
// m3[0,0]==1  m3[0,1]==4
// m3[1,0]==2  m3[1,1]==5
// m3[2,0]==3  m3[2,1]==6

メモリレイアウト

mdspanではレイアウトポリシーLayoutPolicyに従って、多次元インデクスからメモリ領域オフセット位置への変換を行う。同ポリシーはクラステンプレートmapping<Extents>としてレイアウト・マッピング(layout mapping)特性の問合せインタフェースを定義する。

  • required_span_size:変換後にアクセスする可能性のあるメモリ領域の範囲(サイズ)を返す。
  • is_(always_)unique:一意性。異なる多次元インデクスが同じオフセット位置を指さない。ある要素に対応する多次元インデクスが1つに定まる。
  • is_(always_)exhaustive:網羅性。取りうる多次元インデクスに対応する要素位置に “隙間” がない。全次元の要素数の乗算結果がrequired_span_size()に等しい。*8
  • is_(always_)strided:各次元のずらし幅(stride)とインデクス値からオフセット位置を算出。例:3次元ビュー(L x M x N)インデクスi,j,kより((i * M) + j) * N + k

C++標準ライブラリ提供のレイアウトポリシー特性:

ポリシー unique exhaustive strided
layout_right true true true
layout_left true true true
layout_stride true (値に依存) true

std::layout_stride::mapping<Extents>is_exhaustiveはオブジェクトの値(各次元のずらし幅)に依存する。is_always_exhaustivefalse固定。

using Extents = std::extents<int, 2, 3, 4>;
using Mapping = std::layout_stride::mapping<Extents>;
static_assert(not Mapping::is_always_exhaustive());

std::array<int, 3> strides{12, 1, 3};
Mapping mapper{Extents{}, strides};
assert( mapper.is_exhaustive() );
// mapperは3次元インデクス [i,j,k] のうち
// jの次元を連続メモリ配置(strides[1]==1)
// strides{12, 4, 1}はstd::layout_right相当
// strides{1, 2, 6}はstd::layout_left相当

std::array<int, 3> sparse_strides{1, 4, 16};
Mapping sparse_mapper{Extents{}, sparse_strides};
assert( not sparse_mapper.is_exhaustive() );
// sparse_mapperは3次元インデクス [i,j,k] のうち
// i, jの次元末尾にそれぞれ隙間(padding)が存在

// NG: layout_stride変換は常にuniqueであること
std::array<int, 3> bad_strides{1, 1, 1};
Mapping bad_mapper{Extents{}, bad_strides};
// 事前条件(Preconditions)違反により未定義動作

C++2c以降に向けた提案文書P2642では、BLASLAPACKライブラリがサポートするオーバーアライン(overaligned)・レイアウトに対応したlayout_left_padded, layout_right_paddedが検討されている。*9
2024-06-04追記:2024年3月会合でP2642R6が採択され、C++2c(C++26)標準ライブラリにlayout_left_padded, layout_right_paddedが追加される。

関連URL

*1:std::string_view(C++17)やstd::span(C++20)と同様の設計。

*2:C++2bに向けて採択済み(PDF)P2128R6の利用。std::mdspan のために提案されたC++言語拡張。C++20現在はP1161R3により operator[] 内でのカンマ(,)利用が非推奨(deprecated)とされている。

*3:Mandates: IndexType is a signed or unsigned integer type

*4:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2553r1.html

*5:提案文書P0009R18 §2.5引用:The fundamental reason to allow expressing extents at compile time is performance.

*6:提案文書P0009R18 §2.6引用:"Custom" layouts besides these could include space-filling curves or "tiled" layouts.

*7:https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0367r0.pdf

*8:提案初期は contiguous となっていたが、P2604R0にて exhaustive へと変更された。

*9:C++2b時点でも std::layout_stride を用いてBLASLAPACK互換メモリレイアウトを表現できる。P2642では専用レイアウト型を用意することで最適化促進を目指している。