【入門編】配列の形状引継ぎ(Assumed-Shape)と明示的形状(Explicit-Shape)の性能差 – モダンFortran言語仕様と実践実践マスター

「配列の渡し方」で計算速度が劇的に変わる?Fortran最適化の深淵へようこそ

こんにちは!元宇宙航空研究機関で、数十年もの間、スパコンの冷却ファンが唸りを上げる中でコードを磨き続けてきた「Fortranの門番」です。

CやPythonからFortranの世界に足を踏み入れた皆さん、ようこそ。Fortranは「古い」と思われがちですが、実は現代の数値計算において、コンパイラが最もその性能を極限まで引き出せる「モンスター級のツール」なんです。

今日は、皆さんが必ず直面する「配列の引数渡し(Assumed-Shape vs Explicit-Shape)」という、パフォーマンスの分水嶺についてお話しします。ここを理解するだけで、皆さんの書くコードは「動くコード」から「突き抜けるコード」へと進化します。

1. 「見た目」だけの違いじゃない:二つの渡し方

Fortranでサブルーチンに配列を渡すとき、大きく分けて二つの流儀があります。

昔ながらの「明示的形状 (Explicit-Shape)」

subroutine compute_explicit(a, n)
integer, intent(in) :: n
real(8), intent(in) :: a(n) ! 形状を明示的に指定
! …
end subroutine

これはC言語の `void func(double a, int n)` に近いですね。ポインタとサイズを別々に渡す、ある意味で「泥臭い」やり方です。

モダンな「形状引継ぎ (Assumed-Shape)」

subroutine compute_assumed(a)
real(8), intent(in) :: a(:) ! 形状は親から引き継ぐ
! …
end subroutine

こちらがモダンFortranの流儀です。`:` を書くだけで、コンパイラが「この配列のサイズは呼び出し元を見ればわかるから、自分で管理しておくよ」と言ってくれる、非常に賢い仕組みです。

2. なぜ「形状引継ぎ」で性能が落ちることがあるのか?

一見、モダンな `Assumed-Shape` の方がエレガントですよね。しかし、現場では「なぜか `Explicit-Shape` の方が速い」という現象に遭遇することがあります。

その犯人は「一時配列(Temporary Array)」の隠れた生成インターフェースの欠如です。

配列断面(Array Sections)という罠

例えば、メインプログラムで `call compute_assumed(a(1:100:2))` のように、配列の一部だけを飛ばし飛ばし渡したとしましょう。Fortranコンパイラは、`Assumed-Shape` の引数としてこれを受け取る際、メモリ上に連続したデータが存在しないため、「メモリ上に連続した新しい配列を確保し、データをコピーしてからサブルーチンを呼び出す」という処理を裏で行うことがあります。

大規模なシミュレーションでこれをやると、コピーのオーバーヘッドだけで数ミリ秒〜数秒が溶けます。数値計算において、この「メモリのコピー」は死を意味します。

3. 現場の知恵:こうすれば最強になれる

では、どう設計すべきか。以下のステップを意識してみてください。

ステップ1:インターフェース(Module)を必ず使う

`Assumed-Shape` を使うなら、必ず `module` 内にサブルーチンを定義してください。これにより、コンパイラは呼び出し元とサブルーチンの両方を見渡せるようになり、コピーが不要な場合はインライン展開(コードを直接埋め込む最適化)を積極的に行ってくれます。

ステップ2:`contiguous` 属性を活用する

Fortran 2008以降、コンパイラに「この配列はメモリ上で連続しているはずだ」と教えてやるキーワードが追加されました。

subroutine compute_fast(a)
! 連続していることをコンパイラに確約させる
real(8), intent(in), contiguous :: a(:)
! これにより、最適化の幅が劇的に広がる!
end subroutine

4. 実践的な最適化の指針

もし皆さんが書いているコードが「極限の性能」を要求される物理シミュレーションなら、以下の設定でコンパイルしてみてください(GCC/gfortranの場合)。

最適化フラグの例
gfortran -O3 -march=native -ffast-math -fopt-info-vec-missed main.f90

  • `-O3`: もちろん最高速を目指します。
  • `-march=native`: 今使っているCPUの命令セット(AVX-512など)をフル活用します。
  • `-fopt-info-vec-missed`: これが重要! 「なぜベクトル化(並列演算)できなかったのか」をコンパイラが教えてくれます。これを見て、配列のアクセス順序(Fortranは列優先!)を見直すのです。

最後に:コードは「対話」である

Fortranのコンパイラは、皆さんが書いたコードを「どうやって高速に動かそうか」と必死に考えています。`Assumed-Shape` を使ってコンパイラに情報を与え、`contiguous` でメモリの配置を保証する。これは、コンパイラという優秀な相棒に対する「信頼の証」なんです。

まずは小さなコードで、`implicit none` を忘れずに、一つずつ型を定義することから始めましょう。

分からないことがあれば、またいつでも聞きに来てください。皆さんの書く計算コードが、スパコンの上で軽やかに舞うことを心から楽しみにしています!

コメント

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