覚書。
そもそも:目的は?
あなたが実現したいのは、こんな風にアクセスできる配列:
clTensor3D<double> tensor(B, R, C); tensor[b][r][c] = 123.45;
つまり、**3次元配列を [][][] でアクセスしたい!**ということです。
仕組みざっくり
3次元配列を使いやすくするために、以下の**2段階の中間ラッパー(軽い構造体)**を使います:
tensor[b][r][c] └── tensor[b] → returns BatchProxy └── [r] → returns RowProxy └── [c] → 実際の要素 T&
クラス構造:登場人物まとめ
clTensor3D<T> ← 3次元全体([b][r][c] すべて) └── BatchProxy ← 1バッチ分([r][c]) └── RowProxy ← 1行分([c])
pBatchRow は、clTensor3D<T> 内の1バッチ分のポインタ先頭
RowProxy は、1行の先頭を指す軽い中間オブジェクト
どれも inline で作られ、オーバーヘッドはほぼゼロ
実コードで見る:全体像(簡略)
1. clTensor3D<T>
template<typename T>
class clTensor3D {
T* mpData;
size_t mBatch, mRow, mCol;
public:
clTensor3D(size_t b, size_t r, size_t c)
: mBatch(b), mRow(r), mCol(c) {
mpData = static_cast<T*>(_aligned_malloc(sizeof(T) * b * r * c, 32));
}
// BatchProxy: [b]
struct BatchProxy {
T* pBatchRow; // ← r 行分の先頭
RowProxy operator[](size_t r) const noexcept {
return RowProxy(pBatchRow + r * mCol); // ← r行目の先頭
}
};
// RowProxy: [r]
struct RowProxy {
T* pRow;
T& operator[](size_t c) const noexcept {
return pRow[c]; // ← c列目
}
};
BatchProxy operator[](size_t b) const noexcept {
return BatchProxy{ mpData + b * mRow * mCol };
}
};
各インデックスに対して何が返る?
tensor[b] → BatchProxy{ pBatchRow = mpData + b * r * c }
BatchProxy[r] → RowProxy{ pRow = pBatchRow + r * c }
RowProxy[c] → T& = pRow[c]
pBatchRow の正体
BatchProxy operator[](size_t b) const noexcept { return BatchProxy{ mpData + b * mRow * mCol }; // ←ここ }
pBatchRow は mpData + バッチbの先頭
つまり 全データ配列の中で、b番目のブロックを指している
RowProxy の正体
RowProxy operator[](size_t r) const noexcept { return RowProxy{ pBatchRow + r * mCol }; }
pRow は pBatchRow + r行目
つまり、バッチb内のr行目の先頭
最終アクセス
tensor[b][r][c]
これは展開すると:
tensor.operator[](b) .operator[](r) .operator[](c)
という形で動作していて、最終的には:
*(mpData + b * r * c + r * c + c)
と等価なアドレスにアクセスするだけです!
メリット
[][][] で使える!
オーバーヘッドはゼロに近い(インライン関数だけ)
operator() より可読性がいいと感じる人も多い
まとめ
要素 意味
mpData 全てのデータ(1次元)
pBatchRow バッチbの開始位置
pRow 行rの開始位置
RowProxy 1行を指す軽いオブジェクト
[][][] 実質 *(mpData + offset) と等価
必要ならこの3Dテンプレートクラス、完全実装版も作りますよ。分析対象のコードまとめ
方法1: fcNowBoxxRowxCol() + (r, c) でアクセス
size_t pNowBoxxRowxCol = 0;
inline size_t fcNowBoxxRowxCol(const size_t& b)noexcept {
pNowBoxxRowxCol = pRowxpCol * b;
return pNowBoxxRowxCol;
}
inline T& operator()(const size_t& r, const size_t& c)noexcept {
return mVector[pNowBoxxRowxCol + pCol * r + c];
}
特徴:
バッチbの基底オフセットを一度だけ計算
内側ループは pCol * r + c だけで済む
pNowBoxxRowxColはループ前に準備
最もキャッシュに優しい構造
実質 O(1) のポインタ計算のみで最速級!
方法2: [][][] アクセス (RowProxy 等)
tensor[b][r][c]
特徴:
operator[]が2回呼ばれる(中間オブジェクト生成)
中間オブジェクト(小構造体)はインライン化される
実質 (mpData + offset) へのアクセスと等価
👉 わずかにオーバーヘッドがある可能性 👉 でも現代コンパイラではインライン化により非常に速い 👉 コードは可読性が最高
方法3: operator()(b,r,c) + pRowxpCol
inline T& operator()(const size_t& b, const size_t& r, const size_t& c)noexcept {
return mVector[pRowxpCol * b + pCol * r + c]; }
特徴:
3項演算を毎回行う
ループ内で pRowxpCol * b を毎回計算
pRowxpCol * b がループ内で毎回再計算されるので、 方法1と比べて 無駄な計算がある
方法4: operator()(b,r,c) + pRow * pCol
inline T& operator()(const size_t& b, const size_t& r, const size_t& c)noexcept {
return mVector[pRow * pCol * b + pCol * r + c]; }
特徴:
方法3と同等だが、pRow と pCol が独立に使える構成
pRowxpCol をメンバーに持たないので少しシンプル
方法3と同じ速度特性 微差レベル(コンパイラによりどちらでも最適化される)
方法5: 呼び出し元で clVector<double> Arr(N)
この構文自体はアクセス方法とは関係ないですが、 clVector が1次元配列なら、3次元的に扱うには上記どれかのインデックス方式が必要。
速度比較予測(x64, Release, MSVC)
方法
特徴 相対速度(推定)
備考
1 ループ前計算 + operator(r,c) 最速
ポインタ計算だけ、インライン最適
2 [][][] 形式 準高速
インライン化されればほぼ同等
3 (b,r,c) + pRowxpCol 中速
無駄な乗算が毎回入る
4 (b,r,c) + pRow * pCol 中速
3とほぼ同じ
5 - -
-
他の高速化アイディア(+5)
アイディア1: 事前計算されたポインタによる「行バッファ」
T* pBase = &mVector[pRowxpCol * b]; // b番目の先頭 T* pRow = pBase + pCol * r;
operator()(r,c) も使わず、完全なポインタ操作
高速化限界に近い(ベアポインタ)
アイディア2: SIMD付きコピーや初期化(AVX/NEON)
for(int i = 0; i < N; i += 4) { __m256d data = _mm256_load_pd(&src[i]); _mm256_store_pd(&dst[i], data); }
ループのベクトル化による高速初期化やコピー
[]や()ではなく、配列全体の操作に有効
アイディア3: OpenMPでバッチ並列処理
#pragma omp parallel for for(int b = 0; b < B; ++b){ fcNowBoxxRowxCol(b); for(int r = 0; r < R; ++r){ for(int c = 0; c < C; ++c){ mVector[pNowBoxxRowxCol + pCol * r + c] = ...; } } }
多コア並列で処理高速化(数万要素以上で有効)
Visual Studio では /openmp をONに
アイディア4: ループアンローリング(手動展開)
for(int c = 0; c < C; c += 4){ row[c] = ...; row[c+1] = ...; row[c+2] = ...; row[c+3] = ...; }
命令パイプラインを最適化
コンパイラが自動化できることもあるが、明示的にやると効果大
アイディア5: アクセス順をキャッシュライン順に揃える
c が最内側ループ → OK!(メモリが連続)
NG例:for(c)...for(r)... はキャッシュミス増加
結論(推奨)
速度優先なら:
方法1(fcNowBoxxRowxCol() + (r,c))が最速
読みやすさも残すなら、方法2([][][])もかなり優秀
その他は簡便だが、わずかに遅い(1~3%程度)
必要なら、速度測定用のマイクロベンチマークコードも用意しますよ。
どの方法が「実機で一番速いか」測ってみますか?
目標:
最速のアクセス(ポインタ演算ベース)
Arr[b][r][c] の自然な書き方で簡潔に書きたい
fcNowBoxxRowxCol(b); のような明示的な中間処理を避けたい
clVector<T> ベースの基底クラスとして実装
解決策:3重 [][][] アクセスを可能にする operator[] チェーン構造
ここで提案するのは、ポインタキャッシュによる最大限の最適化と、**直感的な書き方(Arr[b][r][c])**の両立です。
実装構造(テンプレートベース)
#include <memory>
#include <cstddef>
#include <cstdlib>
#include <new>
#include <cassert>
#include <iostream>
// 最下層のアクセス([c])
template<typename T>
struct RowProxy {
T* m_pRow;
inline T& operator[](size_t c) noexcept {
return m_pRow[c];
}
};
// 中間層のアクセス([r][c])
template<typename T>
struct BatchProxy {
T* m_pBatch;
size_t m_pCol;
inline RowProxy<T> operator[](size_t r) noexcept {
return RowProxy<T>{ m_pBatch + r * m_pCol };
}
};
// メインのテンプレートクラス
template<typename T>
class clVector3D {
private:
std::unique_ptr<T[]> m_pData;
size_t m_pBatch, m_pRow, m_pCol;
size_t m_pRowxpCol;
public:
clVector3D(size_t b, size_t r, size_t c)
: m_pBatch(b), m_pRow(r), m_pCol(c), m_pRowxpCol(r * c)
{
m_pData = std::unique_ptr<T[]>(static_cast<T*>(std::aligned_alloc(64, sizeof(T) * b * r * c)));
assert(m_pData != nullptr); // allocation check
}
~clVector3D() {
std::free(m_pData.release()); // ちゃんとaligned_allocに対応
}
// 三重 [] アクセス
inline BatchProxy<T> operator[](size_t b) noexcept {
return BatchProxy<T>{ m_pData.get() + b * m_pRowxpCol, m_pCol };
}
// オプション: operator()(b,r,c)
inline T& operator()(size_t b, size_t r, size_t c) noexcept {
return m_pData[b * m_pRowxpCol + r * m_pCol + c];
}
// サイズ取得
size_t Batch() const noexcept { return m_pBatch; }
size_t Row() const noexcept { return m_pRow; }
size_t Col() const noexcept { return m_pCol; }
// 生ポインタ取得
T* Data() noexcept { return m_pData.get(); }
};
使い方(直感的!)
int main() {
clVector3D<double> Arr(10, 20, 30); // 10バッチ、20行、30列
for (int b = 0; b < 10; ++b) {
for (int r = 0; r < 20; ++r) {
for (int c = 0; c < 30; ++c) {
Arr[b][r][c] = b + r + c;
}
}
}
std::cout << Arr[2][3][4] << std::endl; // 出力確認
}
性能的なポイント
Arr[b][r][c] → 実体はただのポインタ演算です。
RowProxy::operator[] → m_pRow[c]
BatchProxy::operator[] → m_pBatch + r * m_pCol
中間オブジェクト(BatchProxy, RowProxy)はインライン展開されてポインタ加算だけになるので、理論上方法1とほぼ同速。
fcNowBoxxRowxCol() のような明示関数は一切不要!
応用と補足
operator()(b,r,c) も併用できるので、シンプルなループ用に使い分けできます。
aligned_alloc + free を使うことで、SIMD対応のアライメントも万全。
C++17以降なら std::aligned_alloc が使えます(MSVCは _aligned_malloc でもOK)。
要望あれば:
SIMDコピー/初期化対応バージョン
OpenMP対応の並列化版
型が int や bool でも動作する汎用設計
どう?この方式なら、手間なく最速級でアクセスできて、コードもすごく読みやすくなるはず。
もし bool や int 用にチューンしたいなら、それも最適化できますよ!
引用 ChatGPT