SIMDの壁を砕く:コンパイラが「ベクトル化を拒絶する」その瞬間の解剖学
スパコンのピーク性能と実効性能の乖離に絶望したことはないか。数万コアを動員した大規模シミュレーションで、プロファイラを開けば「ベクトル化率 0%」という無慈悲な数値が並んでいる。なぜだ。コードは簡潔で、アルゴリズムも正しい。しかし、コンパイラは牙を剥き、最適化を拒んでいる。
今日は、教科書的な「ループを回せ」という助言を遥かに超えた、メモリレイアウトと依存関係解析の深淵について語る。
1. SIMDを殺す「見えない依存関係」の正体
コンパイラが最も恐れるのは「予測不能な書き込み」だ。特にFortranにおいて、配列のインデックス計算に外部からのポインタや、不透明な引数が絡むと、コンパイラは「ループの反復間でデータ依存があるかもしれない」という最悪のケースを想定する。
例えば、以下のようなコードを書いていないか?
subroutine update_field(a, b, n)
real(8), intent(inout) :: a(:)
real(8), intent(in) :: b(:)
integer, intent(in) :: n
integer :: i
! 致命的:a(i) と a(i-1) の関係が不明確とみなされる可能性がある
do i = 2, n
a(i) = a(i-1) + b(i)
end do
end subroutine
これは典型的な「再帰的依存」だ。だが、実際には依存がない場合であっても、コンパイラがそれを確信できなければSIMD化は停止する。ここで登場するのが `contiguous` 属性と `restrict` 的な暗示だ。
解決策:コンパイラの視界をクリアにする
現代のFortranでは、以下のように記述を強制し、コンパイラの躊躇を排除する。
subroutine update_field_fast(a, b, n)
! 配列のメモリアドレスが連続しており、かつ他の配列と重なりがないことを保証
real(8), contiguous, intent(inout) :: a(:)
real(8), contiguous, intent(in) :: b(:)
integer, intent(in) :: n
! コンパイラに依存がないことを確約させる(副作用がないと断言)
!$omp simd
do i = 1, n
a(i) = b(i) 1.5d0
end do
end subroutine
2. キャッシュラインを意識した「列優先」の真理
Fortranの伝統である「列優先(Column-major)」は、単なる歴史的な慣習ではない。CPUのL1/L2キャッシュライン(通常64バイト)を最大限に活用するための必然だ。
多くの研究者がやってしまうミスが、多次元配列のインデックス順序を無視したループ構造だ。
! 低速:キャッシュミスを誘発する典型例
do j = 1, ny
do i = 1, nx
A(i, j) = B(i, j) + C(i, j)
end do
end do
もし`A(i, j)`が `A(1, 1), A(2, 1), …` とメモリ上に並んでいるなら、内部ループは `i` であるべきだ。スパコンのノード内で数TBのメモリを扱う際、キャッシュミスは計算サイクルを数千分の一まで低下させる。プロファイラ(Intel VTune等)で `L1_MISS` が跳ね上がっているなら、真っ先にここを疑え。
3. コンパイラレポートを「読め」
`ifort -qopt-report=5` や `gfortran -fopt-info-vec-missed` を使ったことはあるか? 多くのエンジニアはビルドログを無視するが、ここに最適化の全てが書かれている。
- `vectorization possible but seems inefficient`: レジスタ圧迫が起きている。ループ内部で一時変数を使いすぎていないか?
- `dependency: assumed FLOW dependence`: 配列のオーバーラップが疑われている。`ivdep` プラグマで強制突破するか、構造を見直せ。
4. ハイブリッド並列化の極意:MPI + OpenMP
数万コアのスケールでは、単なるMPIプロセス数だけではキャッシュの共有効率が悪化する。ノード内はOpenMPで共有メモリを活かし、ノード間をMPIで繋ぐのが鉄則だ。
しかし、OpenMPの `schedule(dynamic)` は要注意だ。動的な負荷分散は便利だが、スレッド間の同期コストとキャッシュの汚染(False Sharing)を招く。計算が定型的なら `static` を使い、メモリアクセスの局所性を維持せよ。
チーフアーキテクトからの助言
最後に、コードをモダンにするということは、単に `module` を使うことではない。「コンパイラが最適化を適用するための情報を、いかに明確にコード上に記述するか」という、マシンの言語への翻訳作業そのものだ。
- `type, bind(c)` を使い、メモリレイアウトをC言語と整合させる。
- `allocatable` 配列を多用し、スタックオーバーフローを回避する。
- 可能な限り `pure` 属性を付与し、最適化の自由度をコンパイラに与える。
これらをやり尽くした後にのみ、スパコンの真の計算能力が貴方の手の内に落ちてくる。最適化とは、計算機に対する誠実な対話なのだ。デバッグの手を止めるな。プロファイラのグラフがフラットになるその瞬間まで。

コメント