【テクニカル・上級編】配列アクセスにおけるストライド(Stride)とキャッシュラインの最適化 – モダンFortran言語仕様と実践実践マスター

物理が許す限界へ:Fortranにおけるメモリアクセス最適化とキャッシュラインの調律

君たちが書いているそのループは、本当にCPUのポテンシャルを100%引き出せているか?

スパコンのランキングを競う時代から、今や「いかに演算器を遊ばせず、メモリバスの帯域を飽和させないか」という、究極のデータ・ストリーム・マネジメントの時代に突入している。数万コアのMPI並列環境で「なぜかスケーラビリティが頭打ちになる」という現象の9割は、計算アルゴリズムの問題ではなく、キャッシュラインを無視したメモリの「跳び越しアクセス(Stride)」が原因だ。

今日は、Fortranにおける列優先順位(Column-major)の鉄則を再定義し、モダンFortranでHPCの限界を突破するための知見を共有する。

1. 列優先順位の「呪縛」とキャッシュラインの最適化

FortranのDNAである列優先順位。多くの教科書では「内側ループを第1添字にせよ」としか書かれていない。だが、現場のアーキテクトが気にするのは、その先にある「キャッシュラインの汚染」だ。

現代のCPUキャッシュラインは通常64バイト。倍精度浮動小数点数(8バイト)ならば、1ラインに8要素しか乗らない。もし君のコードが、メモリ上で離れたアドレスを不規則に参照する「ストライドアクセス」を発生させれば、CPUはたった1つの演算のために64バイトのデータをロードし、大半を捨て去る。これがメモリ帯域を枯渇させ、演算器を「待ちぼうけ」にさせる真犯人だ。

悪い例:ストライドが発生するアクセス

! 第2添字を内側ループで回すと、列をまたぐたびにメモリを大きくジャンプする
do j = 1, n
do i = 1, m
a(i, j) = b(i, j) c(i, j) ! ここは連続アクセスでOK
end do
end do

! 最悪の例:転置行列やストライドアクセス
do j = 1, n
do i = 1, m
a(j, i) = b(j, i) c(j, i) ! 致命的。jが増えるごとに巨大なストライドが発生
end do
end do

2. ループ・タイリングとブロッキングの極意

数万コア規模のHPC環境では、L1/L2キャッシュにデータが収まりきらない巨大な行列を扱う。ここで登場するのがループ・タイリング(ブロッキング)だ。データをキャッシュラインに収まるサイズの「タイル」に分割し、再利用性を極限まで高める。

! ループ・タイリングの実装例
integer, parameter :: block_size = 32 ! キャッシュサイズに合わせて調整
do jj = 1, n, block_size
do ii = 1, m, block_size
! タイル内での計算(キャッシュヒット率が劇的に向上する)
do j = jj, min(jj + block_size – 1, n)
do i = ii, min(ii + block_size – 1, m)
a(i, j) = a(i, j) + b(i, j)
end do
end do
end do
end do

プロファイラ(VTuneやScalasca)を回すと、この変更だけで「Cache Miss Ratio」が劇的に改善するのが見て取れるはずだ。特にGPUアクセラレーションを視野に入れる場合、このタイリングは必須要件となる。

3. レガシーからの脱却:モダンFortranが導く最適化の未来

F77形式の `COMMON` ブロックや固定長配列は、コンパイラの最適化パス(インライン展開やベクトル化)を阻害する最大の障壁だ。モダンFortran(2008/2018/2023)を活用し、コンパイラに「これはベクトル化可能である」というヒントを明示的に与えなければならない。

`CONTIGUOUS` 属性の活用

ポインタや仮引数として配列を渡す際、コンパイラは「メモリが連続しているか」を確信できないことがある。そこで `CONTIGUOUS` 属性を付与せよ。

subroutine compute_kernel(n, arr)
integer, intent(in) :: n
real(8), contiguous, intent(inout) :: arr(:) ! メモリ連続性を保証し、ベクトル化を促進

! コンパイラがSIMD化を確信できるため、AVX-512命令等の生成が容易になる
!DIR$ VECTOR ALWAYS
do i = 1, n
arr(i) = arr(i) 2.0_8
end do
end subroutine

4. チーフアーキテクトからの提言

最後に、現場で生き残るための具体的なスタックを提示する。

1. コンパイルフラグの最適化:

  • `ifort -O3 -xHost -qopt-report=5 -qopt-report-phase=vec`
  • 必ず `-qopt-report` を使い、コンパイラが「どこでベクトル化を諦めたか」を追跡せよ。

2. メモリ境界の整列(Alignment):

  • `!DIR$ ATTRIBUTES ALIGN: 64 :: array` を使い、配列の先頭アドレスをキャッシュラインの境界に合わせる。これだけで、SIMDロード命令の効率が数%変わる。

3. ハイブリッド並列の罠:

  • MPI+OpenMPのハイブリッド構成では、スレッド間のキャッシュ・コヒーレンシが重要になる。`OMP_PROC_BIND=spread` や `OMP_PLACES=cores` を適切に設定し、物理コアへの親和性を強制せよ。

「動くコード」を書くのはプログラマの仕事だ。しかし、「CPUの演算器が空腹を感じないコード」を書くのが、我々数値計算アーキテクトの矜持だ。

君たちのコードが、次回のスパコン性能指標(HPCG等)でベンチマークを塗り替えることを期待している。不明点があれば、またいつでも深淵を覗きに来るといい。

コメント

タイトルとURLをコピーしました