質問とシナリオ
コピーを回避し、行優先および列優先のデータをサポートし、std::mdspanのレイアウト、生存期間、パフォーマンスのリスクを説明する2次元行列インターフェースを設計してください。
これは、C++23、数値計算、画像処理、およびハイパフォーマンスサービスの面接に適した問題です。面接官は、単にメモリ上のコンテナ名を挙げるのではなく、2次元インデックスを線形ストレージ、所有権、およびアクセスパターンと結びつけて説明できるかを見ています。
面接官がテストしていること
std::mdspanが所有権を持たない多次元ビューであり、要素の割り当てや解放を行わないことを理解しているか。layout_right、layout_left、およびカスタムストライド間のマッピングの違いを説明できるか。- バッキングバッファの生存期間、拡張、または再配置に起因するダングリングビュー(dangling view)のリスクを特定できるか。
- レイアウトの選択、走査順序、およびキャッシュ局所性を結びつけて考えられるか。
- 静的エクステント、動的エクステント、および実行時チェックを使用してインターフェースの規約(コントラクト)を表現できるか。
回答前の明確化のための質問
- バッキングデータの所有者は誰ですか?また、ビューが存在する間に呼び出し元がコンテナを拡張または移動する可能性はありますか?
- 行と列のエクステントはコンパイル時に判明していますか?また、1つの関数で多様な形状を受け入れる必要がありますか?
- 入力は行優先、列優先、あるいはパディングやタイリングが施されたものですか?
- パフォーマンス目標は、シーケンシャルスキャン、ランダムアクセス、または異なる実行デバイス間での移植性のどれですか?
30秒の回答フレームワーク
mdspanは、C++23で導入された所有権を持たない多次元配列参照です。データハンドル、エクステント、およびインデックスからオフセットへのマッピングを保持します。まず、所有者を生存させ、アドレスを固定します。次に、layout_right(最右次元で単位ストライド、一般的なCスタイルの行優先の動作)、layout_left(最左次元で単位ストライド、一般的な列優先の動作)、またはlayout_strideを選択します。静的エクステントは型にエンコードでき、動的エクステントにはdextentsを使用します。バッファサイズと生存期間の管理責任は呼び出し元にあります。不整合な走査は正しく動作してもキャッシュに優しくないため、走査は連続した次元に従う必要があります。
ステップごとの詳細な回答
1. ビューとコンテナの分離
mdspanは、1次元のspanに対する多次元版です。インデックス指定、エクステント、およびマッピングを提供しますが、要素を所有しません。ここでの所有者はstorageです:
#include <mdspan>
#include <vector>
std::size_t rows = 3;
std::size_t cols = 4;
std::vector<float> storage(rows * cols);
using matrix_view = std::mdspan<float, std::dextents<std::size_t, 2>>;
matrix_view matrix(storage.data(), rows, cols);
matrix(1, 2) = 7.0f;storageが破棄、移動、または再メモリ確保された後、matrixを使用することはできません。インターフェースではこれらの操作を禁止するか、呼び出し中ストレージが安定しているポインタとサイズのみを受け取る必要があります。
2. レイアウトマッピングの説明
レイアウトポリシーは、論理座標を線形オフセットにマッピングします。layout_rightは最右のエクステントを連続にするため、2次元の走査では通常row、次いでcolumnの順にスキャンします。layout_leftは最左のエクステントを連続にします。layout_strideはパディング、転置ビュー、スライス、またはタイル状ストレージを表現できます。
レイアウトはデータをコピーしたり、行優先データを列優先データに変換したりしません。マッピングが実際のバッファと一致しない場合、ビューは誤った要素を読み取ります。マッピングが正しくても走査がストライドに反している場合、主なコストとしてキャッシュ局所性の悪化が生じます。
3. 静的エクステントまたは動的エクステントの選択
4列の行列などの不変条件は、型にエンコードできます:
using four_column_view = std::mdspan<
float,
std::extents<std::size_t, std::dynamic_extent, 4>>;行数は実行時データであり、列数は型レベルの規約です。完全に動的な形状には、2次元のdextentsエイリアスを使用します。いずれの場合も、構築前にrows * colsが実際のバッファに収まることを確認し、乗算オーバーフローを防止してください。
4. パフォーマンスの主張をストライド測定へ変換する
2つのループを示します。layout_rightビューを行ごとにスキャンして内側の列インデックスが連続するようにします。layout_leftの場合はその走査順序を逆にします。matrix.mapping().stride(i)またはマッピングに必要なスパンサイズを検査して、「キャッシュフレンドリー」が測定可能な仮説になるようにします。固定されたデータサイズとコンパイラオプションを使用して、シーケンシャル、逆順、およびストライド走査をベンチマークします。
5. 境界とアクセサの境界を明確にする
デフォルトの要素アクセスは、自動的な境界チェックを保証するものではありません。製品の規約でチェックが必要な場合は、境界部分で次元を検証するか、チェックセマンティクスを持つアクセサポリシーを提供してください。デバッグ時のアサーションを本番環境の安全性とみなしてはなりません。カスタムアクセサはデバイスポインタやプロキシ参照をモデル化できますが、実装の詳細に入る前にアクセスおよび生存期間の規約を明記する必要があります。
質の高い模範解答
私なら、行列の所有権をvector、配列、または呼び出し元が管理するバッファに残し、アルゴリズムには軽量なビューとしてmdspanを渡します。構築時に両方のエクステントを記録し、それらの積を検証します。ビューはストレージより長く生存してはならず、vectorを再割り当てするような操作を跨いではいけません。レイアウトポリシーは実際のストライドと一致させる必要があります。layout_rightは最右次元が連続するCスタイルデータに適し、layout_leftは列連続データに適し、layout_strideはパディングを処理します。正しいマッピングが遅いストライド走査にならないよう、ループの順序は連続する次元に従わせます。AddressSanitizerを使用してダングリングビューを検出し、次元とストライドのアサーションでマッピングを検証し、固定ベンチマークを使用してレイアウトとループ順序を比較します。
よくある間違い
mdspanを所有権を持つ行列コンテナとして扱い、実際の所有者を忘れてしまう。- 実際のストライドを確認せずに、外部データにデフォルトのレイアウトを適用してしまう。
- 走査順序や連続する次元と速度を結びつけずに、「行優先の方が速い」と主張する。
- vectorの拡張、再配置、または一時配列の破棄を無視する。
- バッファ容量や乗算オーバーフローを無視して、2つのエクステントのみをチェックする。
mdspanが自動的に境界チェックやデータの転置を行うと主張する。
フォローアップの質問と回答
mdspanとspanの違いは何ですか?
spanは、1つの連続した1次元の範囲を記述します。mdspanはさらに多次元エクステント、レイアウトマッピング、およびアクセサを保持するため、多次元座標を線形オフセットにマッピングできます。どちらも基盤となる要素を所有しません。
どのような場合にlayout_strideを選択しますか?
パディング、転置ビュー、スライス、または非連続な次元に使用します。まず各実際のストライドを確定し、次にマッピングされた最大のオフセットがバッファ内に収まっていることを検証します。
関数はビューを返すことができますか?
はい、返されたビューが使用される全期間において所有者が生存し、アドレスが安定している場合に可能です。ローカルvectorのビューを返してはならず、ビューの使用中に再割り当てを許可してはなりません。
レイアウトの選択が効果的であることをどのように証明しますか?
ストライドとアクセス順序を記録し、連続走査とストライド走査のスループット、キャッシュカウンタ、およびスケーリングを測定します。コンパイラ、最適化レベル、および入力分布は固定してください。型名だけでは根拠になりません。
出典: cppreference std::mdspanおよびヘッダーエントリ、WG21 P0009R6「mdspan: A Non-Owning Multidimensional Array Reference」、およびVerve AIの多次元配列に関する面接ディスカッション(完全なURLはmeta.jsonに記録されています)。