【入門編】SAVE属性の副作用とスレッドセーフティ – モダンFortran言語仕様と実践実践マスター

「なぜ動かない?」を防ぐ:Fortranの『SAVE属性』とスレッドセーフティの深淵

こんにちは。かつて宇宙開発の現場で、スパコンのノードを何千と並べて熱流体シミュレーションを回していた者です。Fortranの世界へようこそ。

Pythonで書かれたコードをFortranに移植する際、多くのエンジニアが「なぜか結果がおかしい」「並列化すると数値が化ける」という壁にぶつかります。その犯人の多くが、C言語の`static`変数に近い役割を果たす`SAVE`属性です。

今日は、この「一見便利だが、油断すると足元をすくわれる」SAVE属性の正体と、モダンFortranでどう安全に付き合うかを、現場の泥臭い経験を交えて解説します。

1. SAVE属性とは何か? ― 「記憶喪失にならない変数」

Fortranの手続き(サブルーチンや関数)の中で宣言された変数は、通常、その手続きが終わればメモリ上から消え去ります。しかし、`SAVE`をつけると、「プログラムが終了するまでその値を保持し続ける」という命令になります。

subroutine count_calls()
integer, save :: call_count = 0 ! SAVE属性により、二回目以降の呼び出しでも値が生きている
call_count = call_count + 1
print , “この手続きは “, call_count, ” 回目に呼ばれました”
end subroutine

一見便利ですよね。「前回の計算結果を保持しておきたい」というニーズにはぴったりです。しかし、これが現代のマルチコアCPUによる並列処理(OpenMPなど)の世界に入ると、悲劇の引き金になります。

2. なぜ「スレッドセーフ」ではないのか?

現代の計算機は、一つのプログラムを複数の「スレッド」で同時に動かします。想像してみてください。「一つのメモ帳(SAVE変数)」を、大勢の作業員(スレッド)が同時に書き換え合っている光景を。

スレッドAが計算結果を書いている最中に、スレッドBが割り込んで値を上書きする。あるいは、スレッドAが読み込もうとした瞬間にスレッドBがクリアしてしまう……これが「データ競合(Race Condition)」です。

現場からの教訓:RECURSIVEとSAVEの相性

`RECURSIVE`手続き(自身を呼び出す関数)で`SAVE`を使うのは、火に油を注ぐようなものです。再帰的に自分自身を呼び出すたびに、同じ`SAVE`領域を共有してしまうため、入れ子になった計算が互いの値を破壊し合います。

「再帰」を使うなら、変数はスタック領域(SAVEなしのローカル変数)に置くのが鉄則です。

3. どう書くのが「モダン」か?

では、どうすれば安全にデータを保持しつつ、並列化の恩恵も受けられるのでしょうか。答えは簡単です。「SAVEを安易に使わず、状態を外から渡す」ことです。

推奨される実装例:モジュールと派生型(Derived Type)

現代のFortranでは、状態を保持する変数を`MODULE`内の変数に隠すのではなく、`TYPE`(構造体のようなもの)として定義し、それを呼び出し側が保持するようにします。

! 状態を保持するための型を定義
type :: CalculatorState
integer :: call_count = 0
end type

subroutine perform_calculation(state)
! SAVEを使わず、引数として状態を渡す
type(CalculatorState), intent(inout) :: state

state%call_count = state%call_count + 1
! これならスレッドごとに別々のstateを持てるため、並列化しても安心!
end subroutine

こうすることで、並列化する際も`!$OMP PARALLEL PRIVATE(my_state)`のようにして、スレッドごとに独立したメモリ空間を割り当てることが可能になります。

4. コンパイラを味方にするための「泥臭い」設定

現場では、コンパイラに「危ない書き方をしたら教えてくれ」と頼むのが一番の近道です。例えば、Intel Fortran (ifort/ifx) や gfortran を使う場合、以下のフラグを検討してください。

  • `-check stack`: スタックメモリのオーバーフローを検知します。再帰や深いネストのデバッグに必須です。
  • `-qopenmp` (または `-fopenmp`): OpenMPを有効にするだけでなく、並列化時の競合をチェックするツール(Intel Inspectorなど)と併用するのが、宇宙開発級の品質を出すための鉄則です。

まとめ:明日からこう書こう!

1. 安易な `SAVE` は禁止: 「変数をずっと残したい」と思った瞬間、それが本当にスレッド間で共有されても良いものか、一歩立ち止まって考えてください。
2. 引数で渡す: `SAVE`で隠すのではなく、引数(`intent(inout)`)で明示的に渡す設計にしましょう。これが疎結合で、テストしやすく、並列化に強いコードの第一歩です。
3. ローカル変数はスタックへ: `RECURSIVE`手続きを書くときは、自動変数の恩恵をフルに使いましょう。

Fortranは古い言語だと言われがちですが、メモリレイアウトを意識した設計は、今のハードウェア性能を極限まで引き出すための最強の武器になります。

まずは、皆さんのコードにある `SAVE` を一つずつ剥がしていくところから始めてみませんか? きっと、驚くほど安定した計算結果が返ってくるはずですよ。応援しています!

コメント

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