派生型(Derived Type)のメモリレイアウト:キャッシュを支配する者がシミュレーションを制す
数値計算エンジニア諸君。君たちが書いているその`TYPE`定義、本当に「計算速度」を考慮しているか?
「メモリなんてハードウェアが勝手に面倒を見てくれる」という幻想は捨てたまえ。我々が扱う大規模な流体解析や構造解析において、メモリレイアウトの最適化を怠ることは、CPUをわざわざ低速なメモリアクセス待ち(ストール)という地獄に突き落とす行為に等しい。
Fortranの派生型は、C言語の`struct`と同じようにメモリ上に連続して配置される。しかし、ここにはコンパイラによる「構造体パディング(Padding)」という見えない罠が潜んでいる。今日は、このパディングを制御し、キャッシュ効率を極限まで引き出すための「データ設計の美学」を伝授する。
—
1. パディングの正体と「整列」の鉄則
コンパイラは、CPUがアクセスしやすいように、メンバ変数の境界をアライメント(通常8バイトまたは16バイト境界)に合わせようとする。例えば、`REAL(8)`と`LOGICAL(1)`を交互に並べれば、コンパイラは`LOGICAL`の後に無駄なパディングを挿入し、メモリ消費を増やした挙句、キャッシュラインを無駄に消費させる。
鉄則:メンバは「サイズが降順」になるように配置せよ。
8バイトの倍数(`REAL(8)`, `INTEGER(8)`)を先に、次に4バイト、最後に1バイトという順序で並べることで、パディングによる隙間を最小化し、キャッシュラインへの収まりを劇的に改善できる。
—
2. 実践的実装:キャッシュフレンドリーな構造設計
以下のコードを見てほしい。これは、ある粒子法シミュレーションで用いる「粒子状態」を保持する派生型の改善例だ。
module particle_mod
implicit none
private
public :: particle_t
! [改善後] メンバのサイズ(データ幅)で降順ソート
! これにより、アライメントパディングを最小化し、
! キャッシュラインへの充填効率を最大化する。
type, public :: particle_t
real(8) :: pos(3) ! 8バイト 3 = 24 bytes
real(8) :: vel(3) ! 8バイト 3 = 24 bytes
real(8) :: mass ! 8バイト
integer(4) :: id ! 4バイト
logical :: is_active ! 1バイト
! コンパイラはここで 3バイトのパディングを自動挿入して8バイト境界に合わせるが
! 全体で見ればパディングは最小に抑えられる
end type particle_t
end module particle_mod
3. なぜこの「並び順」が重要なのか
もし君がこの粒子を数百万個格納する配列 `type(particle_t), allocatable :: particles(:)` を確保し、ループ内で `particles(i)%pos` や `particles(i)%vel` に頻繁にアクセスするとしよう。
- 構造体の配列(AoS: Array of Structures): 上記のコードのように定義すると、1つの粒子データがメモリ上で物理的に隣接する。データローカリティは向上するが、ベクトル化(SIMD)の際にはメンバごとのストライドアクセスが発生する。
- 配列の構造体(SoA: Structure of Arrays): 極限の性能を追求するなら、`type(particles_t)` のメンバをバラバラの配列にする手法が定石だ。しかし、コードの保守性が著しく落ちる。
私の推奨:
まずは上記のように「サイズ降順」でAoSを設計し、`!$OMP SIMD` やコンパイラ最適化フラグ(例:Intel Fortranなら `-O3 -xHost -qopt-report`)でベクトル化が効くかを確認せよ。性能が飽和した時、初めてSoAへのリファクタリングを検討すべきだ。
—
4. 最適化を加速させるコンパイラ指示子
コードの堅牢性を保ちつつ、コンパイラの最適化をブーストさせるための設定だ。
! ループのベクトル化をコンパイラに強く推奨する
! 構造体の配列アクセスにおいて、ストライドが一定であれば非常に効果的
!DIR$ VECTOR ALWAYS
do i = 1, n_particles
particles(i)%pos = particles(i)%pos + particles(i)%vel dt
end do
推奨ビルド設定(Intel Fortran / ifort, ifx)
- `-O3`: 高度なループ変換とベクトル化を有効化。
- `-align array64byte`: 配列の先頭を64バイト境界(キャッシュライン)に合わせる。これだけで、SIMDロード時のペナルティが激減する。
- `-qopt-report=5`: どのループがベクトル化され、どのループがパディングの影響でストールしているか、魂を込めて解析せよ。
—
最後に:エンジニアへの提言
メモリレイアウトは、単なる「記憶場所の確保」ではない。それは、計算機という巨大な機械に対する、君からの「データ配置の指示書」である。
「なんとなく書いた構造体」を「計算機が最も愛する構造体」へと変える。この一手間を惜しまない者だけが、計算時間を半分に削り、シミュレーションの解像度を倍にする権利を得るのだ。
次回のブログでは、`POINTER` 属性と `CONTIGUOUS` 属性を組み合わせた、動的メモリ確保における「メモリアクセスの連続性保証」について深く掘り下げよう。現場からは以上だ。コードを書き続けろ。

コメント