三つの記憶領域
一般に最近のプログラム言語では、以下の三つの別々の記憶領域に変数を分けて配置しているようです。
-
静的記憶領域 (static)
-
スタック領域 (stack)
-
ヒープ領域 (heap)
Fortran もこれら三種の記憶領域を利用しています。
Classical Fortran から Modern Fortran になるにつれて、コンパイル時に定まる(静的)ものの他に、実行時になって定まる(動的)な機能が増えています。記憶領域(メモリー)の確保もその一つです。
本記事ではこれらの Fortran での使い分けについて考えてみることにします。
Fortran での記憶領域
ここで 1. 静的記憶領域はコンパイル・リンク時に確定され、プログラム実行の初めから終わりまで一貫した番地 (address) に確保されるもので FORTRAN 66 までの FORTRAN でおなじみのものです。Fortran では、主プログラムの変数や save 属性を付けた変数がこれに当たります。
これに対して 2. スタック領域と 3. ヒープ領域 は、実行時に(動的に)確保されるもので、一時的に確保され、有限の期間(寿命)ののち解放されて再利用されます。
スタック領域に確保される変数の典型例は、副プログラム(subroutine/funcition)中の、引数になっていない局所変数 (local variable) で、自動変数 (automatic variable) と呼ばれているものです。
ヒープ領域に確保される変数の典型例は pointer/allocatable 属性を持つ変数で、典型的には allocate 命令により確保され、deallocate 命令で解放されます。
Fortran は、いわゆる GC (Garbage Collection) 機能を備えた言語ではなく、基本的には明示的にヒープ変数の確保・解放を行いますが、pointer の他に allocatable 属性があって、変数のスコープに依存して自動で解放を行える仕組みも用意されており、目的によって使い分けが可能になっています。
GC は負荷が重く、C/C++ や最近の Rust 言語など実行パフォーマンス重視の言語では GC を採用していません。Julia 言語は GC を利用していますが、パフォーマンスを上げるために GC が動作しないように工夫することが薦められているのを見かけます。Fortran が GC を採用しないのも合理的な判断かと思われます。
静的記憶領域
静的なのはサイズや番地であってその変数の内容ではありません。
静的記憶領域の場合、コンパイル時にサイズが確定していなければならず不便な面もありますが、動的に記憶領域の確保・解放を行わないので負荷が無く、実行パフォーマンス重視の HPC おじさんなどは好んで使います。
また記憶領域の少ない時代に書かれたプログラムでは、プログラムの進行に従って大きな静的 COMMON 領域の切り分けを変えて記憶領域の再利用を行う、手動 GC をやっているものも多いと思います。
注意すべき点としては、 FORTRAN 77 は規格上、副プログラムの変数は自動変数になっており、名前付き COMMON も静的と限らないことになっているのですが、過去互換性からコンパイラのデフォルトのオプションが静的確保になっている場合もあり、プログラムもそれを期待している場合があるので注意が必要です。
サブルーチンの局所変数が save 属性になっている前提で、前回呼び出し時の値が残っていることを仮定している 77 プログラムがあったりします。
また、処理系によっては静的配列として、符号付き 32 bit 整数の最大値である 2Gbyte までしか定義できないものがあり、これより大きいサイズは allocatable 属性の配列としてヒープに割り付けねばならないこともあります。
スタック領域
スタック領域はその名前の通り、確保される順に変数領域が積み上げられ、利用後は逆順で解放される一時記憶領域です。解放順序が簡単で、素早く記憶領域を確保できる利点があります。
Fortran では、副プログラム (subroutine/function) の局所変数や、Fortran 2008 で導入された block 構造中の局所変数の確保に使われます。局所変数の確保は入れ子状になっているため、変数スコープと寿命を一致させることが可能で、スタックが適切な実現法になっています。コンパイラが暗黙裡に局所変数を確保・解放するので自動変数と呼ばれます。
またスタック領域は、関数の戻り値を返すためや全配列操作の過程で必要となる一時配列の確保にも使われます。これもコンパイラが暗黙裡に確保・解放を行います。
ところで、Fortran で用いる配列はサイズが大きいことが多いため、局所変数に大きな配列を確保しようとしたり、関数が大きな配列を返そうとしたり、全配列操作で大きな一時配列が必要となる式を書くと、実行時にスタック領域が不足して stack overflow で実行時エラーによって実行が中止されます。
スタック領域は、全体の大きさをリンク時に指定して静的に確保する必要があり、この量を超えて使うことが出来ません。一つの解決法は、スタック領域を大きく確保することですが、無闇に大きく確保するとその分プログラムサイズが大きくなる場合があります。
最近のコンパイラでは、配列が自動変数や一時変数になる場合、スタック領域ではなくヒープ領域に確保(allocate:割り付け)することで、スタック領域が不足することを回避するものがあります。ところが、ヒープ領域は確保に負荷がかかるという欠点もあります。このため、煩雑にヒープ領域の割り付け・解放が繰り返されると実行パフォーマンスに悪い影響が出るおそれがあります。
インテルのコンパイラなどでは、デフォルトではスタックへの確保ですが、コンパイル時のオプションによって、指定のサイズ以下の配列はスタックへ、指定サイズ以上の配列はヒープに確保/割り付けするようになっています。gfortran はデフォルトで配列をヒープに割り付けているのではないかと思います。
ヒープ領域
ヒープ領域は、スタックと異なり任意順で記憶領域を割り付け・解放でき、確保できる大きさの制限も緩くなっていますが、その分仕組みが複雑化して記憶領域の割り付け・解放のための負荷が重くなっています。また暗黙裡に確保されるスタックと異なり、ユーザー側が明示的に割り付け・解放するのが基本になっています。しかしながら Modern Fortran が動的機能を強化するにつれて、だんだんと半自動的に割り付け・解放がなされる場面も増えています。
Fortran では、ヒープに記憶領域を確保するため変数に pointer 属性と allocatable 属性の二つが用意されています。Fortran 90 の時点では、これらの二つに本質的には大きな違いはなかったのですが(allocatable は配列確保に機能限定)、Fortran 95 以降 allocatable の機能が徐々に強化されてスマート・ポインタと称されるものに近づいています。また pointer も機能が強化され、関数の戻り値として返せるようになり、代入式の左辺に置けるような関数も作れるようになりました。
pointer と allocatable は機能がかなり異なってきており、いたずらに pointer 利用を忌避すべきものでもなく、用途に合わせて使い分けるのが望ましいと思われます。Fortran では pointer は記憶領域の番地を共有するだけの shallow copy、allocatable は記憶領域の中身を丸々コピーする deep copy が原則になっており、この点が使い分けの分岐点になると思います。
Fortran 95 以降、allocatable 属性を持つ変数は、副プログラムや block 構造内で定義された場合、スタックに確保される変数のように、それらのスコープを抜ける際に自動で解放されるようになりました。allocatable の場合、linked list のように数珠つなぎ割り付けられた変数も 、根元を解放すると自動でそこから延びている分を解放してくれます。Fortran 90 ではスコープを抜ける際に明示的な解放が必須であったことに比べて、大きな違いになっています。
さらに Fortran 2003 以降、allocatable 属性をもつ変数では代入時の自動割り付けがなされるようになり、割り付けも自動で行われるようになりました。
左辺側の allocatable 配列への代入時の再割り付けを防ぐには、左辺側の配列に (:) をつけて部分配列指定を行います。
なお allocatable では shallow copy は出来ませんが、move_alloc 命令により記憶領域の中身を動かさずに shallow copy のように番地だけを移すことが出来ます。
deep copy が原則で、解放も自動でやってくれる allocatable の方が pointer に比べて、機能も安全性も高く直観的に理解しやすいのですが、負荷が重いという欠点もあります。
例として配列を戻り値として返す関数を考えた場合、自動変数や allocatable で返すと、配列全体の deep copy が行われてしまい負荷が重くなりますが、pointer で返せれば shallow copy で済むので負荷が軽くなります。
演算子のオーバーロードを用いる場合、subroutine の引数で返す方法が使えないので ponter を返す関数が重要になると考えられます。
結論
Modern Fortran は動的な機能を強化しているので、記憶領域の確保の仕方も多様になってきており、目的に合わせて使い分けるのが好ましいと考えられます。
例えば Stack Overflow の回避方法も、stack サイズを増やしても良いし、暗黙裡にヒープに取るようにしても良いし、プログラム中で明示的にヒープに割り付けても良く、利害得失を見て最も好ましい方法を取るのが良いのではないかと思います。
また pointer と allocatable は機能がかなり異なってきているので、Fortran の pointer は安全性や最適化のために自由度が著しく限定されていることを考えると、他言語の生 pointer の様にいたずらに利用を忌避するのではなく、用途に合わせて利用すればいいのではないかと思います。
おわび
本当は実例を用意しながら、話を進めたかったのですが、時間の関係で言葉だけになってしまいました。