メモリ上での配置に関して、多次元配列と構造体の配列の比較

最近少しアライメントのことを考えているが、古めの書籍だと多次元配列のメモリ配置の説明が必ずと言っていいほどされていて、メモリアクセスが律速になる場合(殆どのケースだ)には次元の順序に十分注意せよと書かれている。

キャッシュ効率のことなどを考えるとこれは非常に重要だ。メモリ上で連続した値に順にアクセスしていく場合、飛び飛びにアクセスするよりもキャッシュが効いて速くなることが多い。(例えば、Fortran Tip集: 多次元配列のアクセス順序に注意する

簡単に言うと、以下のようなデータがあって、

{ {x1, y1, z1}, {x2, y2, z2}, ... {xN, yN, zN} }

今は全てのデータについてZの値だけを見ていきたいとする。 もしこれがメモリ上で以下のように並んでいたなら、

|x1|y1|z1|x2|y2|z2|x3|y3|z3|...

z1とz2の間には(今は)必要のないデータが挟まっていて少し距離があるので、単純に近い領域のものを一緒に持ってくるキャッシュ戦略では効率が悪くなる。

以下のように並んでいたなら、

|x1|x2|x3|...|xN|y1|y2|...|yN|z1|z2|...|zN|

Zの値を取得する場合は必要な値がメモリ上で近くにあり、キャッシュ効率が上がる結果処理速度が高まるだろう。 そういうことをちゃんと考えた上で、多次元配列の宣言は上手くやれというのがよくあるTipsだ。 私はそれ自体に異を唱える気は全くこれっぽっちもないのだが、多次元配列のメモリ上での配置でどっちの次元が先に置かれるとかを覚えるのは苦痛ではないかと思う。

普通に構造体を作ったらどうなるだろう。

struct vec{float x, y, z;};
std::vector<vec> vs;

vsの中身がどうなっているか、迷うだろうか。vectorは要素を一つずつ配置していくだろうから、一つのvecの後に別のvecが続くだろう。どういう配置になるかは考えるまでもない(パディングが予測できないではないか、と思ったあなたは鋭い。そこまで知っているなら今回の記事の対象読者ではない。どんな状況でもイメージが浮かぶだろうから好きにやっていただきたい。一応言及しておくが、alignas__packedなどを使って制御する手があることも知っておられるだろう)。 逆向きの配置を使いたかったらどうするか。

std::vector<float> x;
std::vector<float> y;
std::vector<float> z;

これでも似た感じになるが、xNの次にy1が来ることは保証されない、というかほぼないだろう。一応

std::size_t N;
std::vector<float> x_y_z(3*N);

のようにしてオフセットを使えば完全にxNの次がyNのようにできるが、個数が変わったりすることを考えると悪夢だし(xNの次に追加の要素を入れる場合、後続のyとzを全てずらさなければならない)、オフセットを制御するメンバでも噛まさない限りバグを仕込む可能性は高まるだろう。Zの値だけを必要とする場合、別にXとYも同時に見ないといけないわけではないので、単に配列を3つ持っていればよいと思う。

もちろん、xi, yi, ziを同時に見る必要が生じる場合、それぞれの場所が離れていればあまりうれしくないだろうから、std::vector<vec>の形になっていた方が幸せだろう。要するにする計算に応じて必要になる可能性が高いものを近くに置くべきなのだ。

何にせよ、どちらを優先するにしても、配置に気を配れる方がよい。そして、「できれば〜したほうが良い」をほぼ常に行わせる唯一の方策は、それを簡単にすることだ。多次元配列でどの次元が先に来るかを学んで(そもそもそういうことが起きることを知って)、それを常に思い出すよりは、配列には常に要素型が順に並べられるとだけ思って構造体を並べるか配列を並べるか考えた方が、イメージが楽だろう。

というわけで、最終的に構造体を使うことに決めるかは置いておいて、多次元配列ではなく構造体を使うことが最初に選択肢として挙がっていた方がその後も捗るのではないかと思っている。

注:関連した話なので飛ばし読みして混同する人が出るかも知れないが、AoS、SoAで速度を考えた時常に構造体の配列がよいと言っているわけではない(ちゃんと読んだ人はそんな風に解釈しないだろうが)。それはメモリアクセスのパターンとアーキテクチャに依存する話なのでそんなことが言えるわけはない。今はイメージしやすいかどうかの話をしている。多次元配列のメモリ配置を覚えるのに比べれば、構造体を定義した場合のそれをイメージする方が簡単だ、だから速度的に最終的に使わないことに決めるとしても、構造体を使うことを選択肢に入れた方がよいという話だ。