配列セクションの罠:スパコンの演算器を「飢え」させないためのメモリレイアウト極意
Fortranを「古い言語」と呼ぶ連中は、L1キャッシュのラインフィルがどう行われるか、あるいはAVX-512のマスクレジスタが一時配列の生成によってどう無力化されるかを知らない。我々が扱う数万コア規模のHPC環境において、`A(1:N:2)` といった単純な配列セクション記法は、単なるシンタックスシュガーではない。それは、コンパイラの最適化エンジンに対する「宣戦布告」にもなれば、「最適化への招待状」にもなり得る諸刃の剣だ。
今日は、教科書には載っていない、現場の泥臭いメモリ最適化の話をしよう。
1. コンパイラが「一時配列(Temporary Array)」を生成する瞬間
多くの開発者が陥る罠は、配列セクションを引数として渡す際に、コンパイラが裏で「コピー」を作成している事実に気づかないことだ。
特に、明示的なインターフェース(`interface`ブロックやモジュール内の手続き)がない、あるいは手続き側が配列の形状(shape)を固定で受けるように設計されている場合、コンパイラはメモリの不連続性を解消するために、一時的なワーク配列をスタック(またはヒープ)に確保する。
! 最適化の敵:不連続なアクセスによる一時配列の生成
subroutine process_data(arr)
real(8), intent(in) :: arr(:)
! この手続きが仮引数として「連続な配列」を想定していると、
! コンパイラは呼び出し元でA(1:N:2)をコピーして連続メモリを作る
…
end subroutine
! 呼び出し側
call process_data(A(1:N:2)) ! ここでメモリアロケーションが発生する可能性大
数万コアでこれをやれば、ノードごとのメモリ帯域はコピー処理で飽和し、演算器はデータ待ちで「飢え(Starvation)」を起こす。VTuneでプロファイルした際、`Memory Bound`の指標が真っ赤になる原因の9割はこれだ。
2. ベクトル化を殺さないための「Contiguous」属性
Fortran 2008以降、我々は `contiguous` 属性という強力な武器を手に入れた。これを明示することで、コンパイラに対して「この配列はメモリ上で物理的に連続している」という強い制約を課すことができる。
もし手続き側で `intent(in) :: arr(:)` と書くならば、必ず以下のように記述せよ。
subroutine process_optimized(arr)
! メモリ配置の連続性をコンパイラに保証させる
! これにより、コンパイラはコピーなしでSIMD最適化を適用できる
real(8), contiguous, intent(in) :: arr(:)
! ループ内でSIMDレジスタ(ZMM等)へ効率的にロードされる
!$omp simd
do i = 1, size(arr)
arr(i) = arr(i) 2.0_8
end do
end subroutine
これを行わない場合、コンパイラは「もしかしたら不連続かもしれない」という疑念を捨てきれず、ループ展開(Unrolling)やベクトル化を控えめに実行する。あるいは、実行時チェックで膨大なオーバーヘッドが生じることになる。
3. ストライドアクセスとキャッシュラインの攻防
`A(1:N:2)` という記述は、ストライド(Stride)が2であることを意味する。CPUキャッシュラインが64バイト(`real(8)`なら8要素分)であることを考えると、ストライド2でのアクセスは、キャッシュラインの利用効率を理論上50%に落とす。
さらに深刻なのは、ストライドがキャッシュラインのサイズ(8や16の倍数)に近い場合、キャッシュラインの「競合(Conflict Miss)」が発生することだ。特定のバンクにアクセスが集中し、メモリコントローラが悲鳴を上げる。
実践的対策:ブロッキング(Blocking)
大規模行列演算においては、配列セクションをそのまま処理するのではなく、キャッシュサイズに合わせた「タイル(Block)」分割が必須となる。
! キャッシュミスを防ぐためのタイル化の例
block_size = 64 ! L1/L2キャッシュの容量に合わせて調整
do ib = 1, N, block_size
ie = min(ib + block_size – 1, N)
! このブロック内であれば、キャッシュヒット率は劇的に向上する
call process_block(A(ib:ie))
end do
4. プロファイラでボトルネックを特定せよ
ScalascaやIntel VTuneを使用する際、単に「どこが遅いか」を見るのは素人だ。見るべきは `L1/L2 Cache Miss Rate` と `SIMD Utilization Rate` の相関である。
- L1ミスが多い場合: `contiguous`属性の付け忘れ、あるいはストライドが大きすぎる。
- SIMD効率が低い場合: コンパイラが一時配列生成を避けるために、あえて安全な(遅い)スカラーコードを生成している。
ビルド時には以下のフラグを検討すべきだ(Intel Fortranの例)。
- `-qopt-report=5`: 最適化レポートを出力し、どこで「Vectorization failed」となったかを確認する。
- `-O3 -xHost -qopt-zmm-usage=high`: ターゲットマシンの命令セットを極限まで引き出す。
- `-assume contiguous_assumed_shape`: 隠れたコストを可視化し、連続性を強制的に仮定させる(※ただし自己責任で)。
結論
モダンFortranの魅力は、高レイヤの抽象化と低レイヤのメモリ制御が同居している点にある。配列セクションは便利だが、その裏側でメモリがどう動いているのかを、常に脳内でイメージすること。
「コンパイラがやってくれる」などという甘えは捨てろ。プロセッサのレジスタからメインメモリに至るまでの「データの旅路」を設計してこそ、初めてスパコンの真価を引き出せる。次にコードを書くときは、`contiguous`の重みを再確認してほしい。君の書いた一行が、数千ノードの消費電力を劇的に削減するかもしれないのだから。

コメント