std::mdspan 的進階使用

| | 0 Comments| 09:05
Categories:

之前有介紹過 C++23 的 std::mdspan 了。在當時,是簡單地把它當成 C++20 std::span 的多維度版本。而前陣子更新的 Visual Studio 2022 17.12 也終於支援了多維度的 []、可以用比較標準的方法來存取了~

實際上,mdspan 還可以透過設定成為 std::layout_stride、或是指定自定義的 accessor 來做更有彈性的資料存取。 所以它在使用上的彈性可以說是比僅限於連續存取的 std::span 大了非常多!

不過或許是由於目前 C++23 還算很新(儘管已經 2024 年底了)、再加上各家編譯器對 mdspan 的支援性也還不算很好,所以基本上這類進階使用的文件並不多。

這邊就來稍微整理一下 Heresy 自己玩的結果吧~


layout_stride

透過把 layout policy 設定成 std::layout_stride::mapping文件),可以讓存取時跳過特定的間隔來讀取。下面是一個使用的例子:

#include <array>
#include <mdspan>
#include <iostream>
 
template<typename T>
void output( T view1)
{
  for (size_t i = 0; i != view1.extent(0); ++i)
  {
    for (size_t j = 0; j != view1.extent(1); ++j)
      std::cout << view1[i, j] << " ";
    std::cout << "\n";
  }
  std::cout << "\n";
}
 
int main()
{
  int aData[64];
 
  std::mdspan v1{ aData, 6, 8 };
  for (int i = 0; i < v1.extent(0); ++i)
    for (int j = 0; j < v1.extent(1); ++j)
      v1[i, j] = 10 * (1 + i) + (1 + j);
  output(v1);
 
  std::mdspan v2(aData,
    std::layout_stride::mapping{
        std::extents<int,3,4>(),
        std::array{16, 2}
    }
  );
  output(v2);
}

這樣的結果會是:

11 12 13 14 15 16 17 18
21 22 23 24 25 26 27 28
31 32 33 34 35 36 37 38
41 42 43 44 45 46 47 48
51 52 53 54 55 56 57 58
61 62 63 64 65 66 67 68

11 13 15 17
31 33 35 37
51 53 55 57

這邊 v2 是使用一個 mapping_type 做為建構 mdspan 的資料,而這邊是用 std::layout_stride::mapping 這個結構來描述資料的配置。

這邊除了指定代表維度資訊是 3 x 4 外,還指定了一個 std::array 作為 stride 的資訊;這裡的 std::array{16,2} 代表的是每個 row 的起始位置相差 16、然後一列裡面每個 col 的位置差異是 2;而由於 v1 的每列有 8 個項目,所以 16 就像當於跳過一列來存取了。

而由於 submdspan() 這個功能目前是規劃要到 C++26 才會有,所以如果有需要的話,就需要靠自己算 stride 來實作了~例如把 v2 改成:

std::mdspan v2(aData + 9,
  std::layout_stride::mapping{
      std::extents<int,3,4>(),
      std::array{8, 1}
  }
);

那就會變成是只取其中一部分來看了。

22 23 24 25
32 33 34 35
42 43 44 45

在某些情況下,這樣的使用方法也算是有幫助的。

另外,考慮到 mapping 是放在 std::layout_stride 下,不知道以後會不會有其他的功能?


AccessorPolicy

mdspan 的 accessor policy(文件)是用來定義如何存取底層的一維資料用的,預設會是使用 std::default_accessor文件)。

老實說個人覺得到底要怎麼定義一個自己的存取方法目前相關資料不算多;不過基本上,他需要定義四個型別、以及兩個函式,下面是 Visual C++ 實作的 default_accessor(簡化了一點):

template <class _ElementType>
struct default_accessor {
  using offset_policy     = default_accessor;
  using element_type      = _ElementType;
  using reference         = _ElementType&;
  using data_handle_type  = _ElementType*;
 
  constexpr reference access(data_handle_type _Ptr, size_t _Idx) const noexcept {
    return _Ptr[_Idx];
  }
 
  constexpr data_handle_type offset(data_handle_type _Ptr, size_t _Idx) const noexcept {
    return _Ptr + _Idx;
  }
};

Heresy 自己測試了一下,覺得應該是下面的狀況,如果有錯誤的話也請幫忙指正了。

這邊定義的資料型別有三個:

  • element_type:原始資料的型別
  • reference:讀取出來的資料型別(參考)
  • data_handle_type:內部儲存資料的型別

access() 這個函式就是根據內部資料的指標、以及指定的索引值來讀取資料;如果有比較特別的讀取方法,就是修改這個函式。

另外還有一個函式是 offset()、理論上是用來自定義計算資料怎麼去處理 offset;而 offset_policy 則應該是用來定義要用哪個物件的 offset(),所以預設是自己的型別。

不過這邊在玩的時候,是發現 MSVC 的 mdspan 實作似乎完全沒有用到 offset 的功能?裡面完全沒有去呼叫 offset() 的程式、甚至可以不用定義和 offset 相關的東西。就不知道這是給其他型別用的,還是只是微軟沒有實作了?

總之,下面是一個簡單的範例:

#include <array>
#include <mdspan>
#include <iostream>
 
struct SVec3
{
  float x;
  float y;
  float z;
};
 
template<typename T>
void output(T view1)
{
  for (size_t i = 0; i != view1.extent(0); ++i)
  {
    for (size_t j = 0; j != view1.extent(1); ++j)
      std::cout << view1[i, j] << " ";
    std::cout << "\n";
  }
  std::cout << "\n";
}
 
struct SAccessorX
{
  using offset_policy = SAccessorX;
  using element_type = SVec3;
  using reference = float&;
  using data_handle_type = SVec3*;
 
  constexpr reference access(data_handle_type _Ptr, size_t _Idx) const noexcept
  {
    return _Ptr[_Idx].x;
  }
 
  constexpr data_handle_type offset(data_handle_type _Ptr, size_t _Idx) const noexcept
  {
    return _Ptr + _Idx;
  }
};
 
int main()
{
  std::array<SVec3, 12> aVec;
  for (int i = 0; SVec3 & v : aVec)
  {
    v.x = i;
    v.y = i + 1;
    v.z = i + 2;
    ++i;
  }
 
  std::mdspan<SVec3, std::extents<int, 3, 4>, std::layout_right, SAccessorX> v1(aVec.data());
  output(v1);
 
  auto v2 = std::mdspan(aVec.data(),
    std::layout_stride::mapping{
      std::extents<int,2,2>(),
      std::array{4, 2}
    },
    SAccessorX{});
  output(v2);
}

這邊是拿 SVec3 這個結構當資料來用;而 SAccessorX 則就是自己針對 SVec3 定義的存取用類別,裡面的 access() 基本上就是指回傳 x 這一項;透過 using 定義的 reference 也會變成是 float&

這樣定義之後,在透過 mdspan 重新封包 aVec 這個 SVec3 的陣列的時候,如果有去指定使用 SAccessorX 的話,封包出來的結果就會像是在存取 float 這樣的浮點樹資料了~

不過要指定 accessor policy 也有點麻煩,基本上要不就是得把 mdspan 的 template 引數全部寫完、不然就是得指定 mapping 的方法才行,個人是覺得沒那麼好用。


這篇其實在當時在寫 std::mdspan 的記錄的時候就開始寫了,但是因為編譯器支援度不好、還有就是 accessor policy 沒找到相關資料的關係,其實擱置了好久,前陣子才決定把這篇寫到一定程度來收尾。

總之,這邊也不確定理解到底有沒有錯、之後其他家的實作出來也不知道能不能用?總之,就先當作筆記紀錄一下了。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *