概要
FORTRANコードを書き直す際に役立つFortranの機能を紹介します.本記事において,Modernizeとは,その当時よいとされていた(あるいはそのようにしか組めなかった)コードを現代のスタイルに置き換える行為を意味しています.
FORTRANコードとソフトウェア開発
FORTRANで書かれたプログラム(以下,FORTRANコード)は, 望ましくないコードの例としてやり玉に挙げられます.確かに,現代のプログラミングのスタイルからすると,望ましくない書かれ方をしていることが多々あります.
しかし,望ましくないFORTRANコードが生まれた時点で既に望ましくなかったかというとそうではなく,当時の環境ではそのように書くのが普通でしたし,そのようにしか書けない制約がありました.
それらが望ましくないと言われるようになるまでに,計算機環境,プログラミングパラダイムなど,様々な変化がありました.Fortranもそれらのパラダイムに対応するように規格を更新しています.
FORTRANが嫌いなあまり,またその制約から逃れる道を探して異なるプログラミング言語を使い始め,滅ぼして全てを置き換えよと主張したくなる気持ちもわかります.しかし,問題の原因は言語にはありません.FORTRANで課されていた(現代から見た)制約は既に撤廃されています.問題は,望ましくないコードの更新を妨げる文化にあります.
FORTRANは,C言語と比較して配列の演算を容易に記述することができ,またポインタなど理解が困難な機能を取り扱う必要がありません.そのため,プログラミングを専門としない技術者,研究者の間で愛用されてきました.
企業では,効率的に働くことが求められます.プログラムの機能強化で同じ処理が出てきたとき,まずは関数に分離することを検討するでしょうが,そうすると関数の仕様を決めて仕様書および詳細設計書を書き,テストを行う必要が生じます.そうすると,当たり前ですが,工数が増えますし,バグを埋め込む可能性も0ではありません.既に動いているコードに手を入れ,バグを埋め込んでしまうと,上役や元請けに激詰めされる上に,なぜなぜ分析をして再発防止策を提出し,チェックリストを作るなど余計な仕事が増えます.このような状況では,関数に分離するよりは,コピペをしてしまう方が,その案件を安くこなす上で効率的なのです.
企業・大学にかかわらず,本務以外にプログラミングやその周辺知識・技術を勉強しても,本務で成果を上げなければ一切評価されません.また,研究開発において,誰も実現していない計算手法を開発・実装する際に,適した開発方法論は無いように思われます.これは個人の能力ではなく,数値計算という分野全体に関係しています.特に,いわゆるスキーム屋さんは肌感覚で理解しているでしょう.解決したい問題は分かっていても,解決に至る道筋はおぼろげに見えるだけです.提案した新規アルゴリズムの有用性を,様々な条件のテストを通して明らかにしていくわけですが,実装して初めて判明する問題もあるでしょう.これらは,ソフトウェア開発における要求定義以前の段階です.
だからといって,ソフトウェア工学に関する知識が不要だとは考えていません.むしろ,ソフトウェア工学やソフトウェア開発手法を深く理解し,数値計算に適した形で取り込まなければならない(ならなかった)でしょうし,近視眼的で間違った効率化を追求したソフトウェア開発も改めるべきでしょう.数値計算,特にFORTRANの適用先であるスーパーコンピュータでの大規模計算において,速さが全てで1 msでも遅くなるような機能は不要だと豪語する方もいるでしょうが,ソフトウェアにおいて最も重要なのは,正しく動くことです.そして,プログラムが正しく作られていることを確認し,実装の際に間違いを減らすために,(見た目の美醜ではなく理解しやすいという意味で)美しく書くのです.
硬直化した文化を変化させるのは,容易ではありません.できることからコツコツと進めて行くしかないのですが,その一つがコードのModernizeです.Modernizeはリファクタリングと同じですが,古いパラダイムのコードを現代のパラダイムで置き換えるという行為を,著者が勝手にそう呼んでいます.いくら新しい言語が優秀だからといって,20年間開発され続け,強固に密結合された,設計書もまばらでテストも存在しないコードをいきなり置き換えるには,かなりの時間がかかります.人が動くとなると,その分お金も発生します.一切儲けが出ないリファクタリングを快く許可してくるれることなど,まずありません.その言語を使えるのがあなた一人だけなら,業務の継続という観点ではリスクでしかありません.また,誤った文化を継承した状態で言語だけを変えても,上述の文化が持ち込まれるだけです.せっかく苦労して導入したお気に入りの言語が汚染されていく様子を,悲しげに見つめることになるでしょう.新しい言語が好きな方には,FORTRANにとらわれず,数値計算に適した開発スタイルの開拓に力を使ってくれることを希望します.
著者が考える解決策の一つが,FORTRANコードのFortranによる部分的な置き換えです.Fortranは強力な後方互換性を有していますし,廃止事項もコンパイラベンダが独自にサポートを継続しています.諸手を挙げて歓迎できない状況でもありますが,そのおかげでFORTRANとFortranを混ぜたとしても,コンパイルはほぼ確実に可能です.
FORTRANコードあるある,その要因と解決法
FORTRANコードあるある
FORTRANコードあるあると,それらの置き換え方法を探っていきます.
- 重要な変数全てがグローバル
- 各地にコピペされた同じコード
- 異様に多いソースファイル群
- 短く,意図の読み取れない変数名
- 自在に飛び回る行番号
重要な変数全てがグローバル
計算に必要なパラメータや物理変数を格納する配列をあるモジュールに集約し,procedure(subroutineとfunction)内でuse
して利用するスタイルです.例を以下に示します.これは,common
や静的なメモリプールの使用を止め,脱FORTRANを目指したコードでよくみられます.
program main
implicit none
call umag
end program main
module param
implicit none
integer,parameter :: N = 100
real*4 u(3,N)
real*4 u2(3,N)
integer*1,parameter :: K_ON = 1
integer*1,parameter :: K_OFF = 0
integer*1 :: JQWIOD = K_ON
end module param
subroutine UMAG
use param
implicit none
integer :: i
real*4 :: mag
if(JQWIOD == K_ON)then
do i = 1, N
mag = sqrt(U(1,i)**2 + U(2,i)**2 + U(3,i)**2)
! : magを何かに使う
end do
else
do i = 1, N
mag = sqrt(U2(1,i)**2 + U2(2,i)**2 + U2(3,i)**2)
! : magを何かに使う
end do
end if
end
プログラムを書く側からすると,いつでもどこでも全ての変数にアクセス可能であり,かつprocedureの引数を書かなくて済むので非常に手軽に感じます.一方で,これは変数を全てグローバルにしているのと代わらず,変数がどのprocedureで読み書きされているか不明瞭になります.また,全てのprocedureが密結合になるので,procedureを他のプログラムで再利用することは,ほぼ不可能でしょう.
改善は,2段階で行います.
-
use
にonly
句を付け,利用する変数を可視化 - その後,procedureに引数を設け,密結合を緩和
program main
use param,only: U, U2 ! only句を付けて使う変数を可視化
implicit none
call umag(U, U2) ! 引数を設ける
end program main
subroutine UMAG(U, U2)
use param, only: N, JQWIOD, K_ON ! only句を付けて使う変数を可視化
implicit none
real*4,intent(in) :: U(3,N),U2(3,N) ! 引数にintent属性を付け,用途を明示
integer :: i
real*4 :: mag
if(JQWIOD == K_ON)then
do i = 1, N
mag = sqrt(U(1,i)**2 + U(2,i)**2 + U(3,i)**2)
! : magを何かに使う
end do
else
do i = 1, N
mag = sqrt(U2(1,i)**2 + U2(2,i)**2 + U2(3,i)**2)
! : magを何かに使う
end do
end if
end
subroutine UMAG
内で,引数は形状明示配列U(3,N),U2(3,N)
として受けていますが,引数を形状引継ぎ配列U(:,:),U2(:,:)
として受ければ,N
も排除できます.必要になれば,size
関数を用いて,size(配列,次元)
として取得すればよいでしょう.
何らかの判別を行うパラメータJQWIOD
をlogical
型に変更すると,K_ON
やK_OFF
といった定数の定義も排除できます.これらは論理型がなかった時代によく使われていたスタイルで,接頭辞のK_
はドイツ語のkonstante
に由来するようです.
こうすると,引数が異常に多くなるprocedureが現れます.その際は,同じ物理量(U
とU2
のように,何らかの計算条件によって参照される変数が違う)全てを渡すことを止めることと,構造体の導入を検討してください.大体はそれで解決できます.
各地にコピペされた同じコード
上のumag.f90でおや?と思ったことでしょう.一つのprocedure内や異なるproceduer内で,共通する処理が複数箇所に出現します.
if(JQWIOD == K_ON)then
do i = 1, N
mag = sqrt(U(1,i)**2 + U(2,i)**2 + U(3,i)**2)
! : magを何かに使う
end do
else
do i = 1, N
mag = sqrt(U2(1,i)**2 + U2(2,i)**2 + U2(3,i)**2)
! : magを何かに使う
end do
end if
これは上でも書きましたが,動いている実績のあるコードを使うので,コピペは安全・簡潔で効率的と見なされます.短期的な成果を追い求める余り,(誤解されている方の)DRYの原則が,Don't Repeat YourselfからDuplicated Repeatedly by Yourselfに変質したのでしょう.しかし,この記事を読んでいる全員が理解しているように,コピペをすると同じ処理があちこちに分散し,ある箇所の処理を変更した際に他の箇所の更新忘れが発生します.変更を忘れてもバグを埋め込んでも正しく動かないことは同じですが,変更忘れの方は考慮漏れとよばれる軽微な罪として扱われます.壊したわけではないですし,元請けはコードの詳細を知りませんので,精々使いもしないチェックシートを作る程度で終わります.
これを改善するには,基本に立ち返って共通部分を関数として切り出します.その際,同一手続き内に同じコードが存在する場合には,内部副プログラムが利用できます.内部副プログラムというよりは,クロージャーと呼んだ方が理解しやすい方もいるでしょうか.procedureの中にprocedureを設けることができる機能です.
subroutine UMAG(U, U2)
use param, only: N, JQWIOD, K_ON
implicit none
real*4,intent(in) :: U(3,N),U2(3,N)
real*4 :: mag
if(JQWIOD == K_ON)then
do i = 1, N
mag = computeVelocityMagnitute(U) ! 内部副プログラム呼出に変更
! : magを何かに使う
end do
else
do i = 1, N
mag = computeVelocityMagnitute(U2) ! 内部副プログラム呼出に変更
! : magを何かに使う
end do
end if
contains
! 速度の絶対値を計算する内部副プログラム
function computeVelocityMagnitute(v) result(magnitude)
implicit none
real*4,intent(in) :: v(3,N)
real*4 :: magnitude
integer :: i
magnitude = sqrt(v(1,i)**2 + v(2,i)**2 + v(3,i)**2)
end function
end
内部副プログラムを使うと,ひとまず変更範囲を一つのprocedure内に抑えることができます.これで動作を確認した後,独立したprocedureとして切り出すのです.内部副プログラムでは親スコープの変数も参照可能ですし,クロージャーもそのように使うのでしょうが,後々個別の手続として切り出す可能性が高い場合は,内部副プログラム内で使う親スコープの変数も,引数に入れた方がよいと個人的には考えています.
似たようなことを実現できる機能として文関数もありますが,文関数は既に廃止事項となっているので,使わないようにしてください.洗練された形で復活してくれることを望んではいますが.
異様に多いソースファイル群
ある一定の規模を超えると,FORTRANプログラムはソースファイルが異様に多くなります.これはかつてグッドプラクティスとされていた,1ファイルに1 procedureだけを書くというスタイルの名残です.
program main
implicit none
integer,external :: doublify ! doublifyの定義の参照を阻害
call hello()
print *,doublify(2) !VSCodeで実装の確認のために「定義へ移動」しようとしても,external宣言までしか移動できない
end program main
subroutine hello()
implicit none
print *,"hello"
end subroutine hello
function doublify(i) result(i2)
implicit none
integer :: i
integer :: i2
i2 = i*2
end function doublify
利点としては,あるprocedureの内容を差し替えるのが楽になることと,差し替えたprocedureだけが再コンパイルされるので,コンパイル・リンク時間が短くて済むということが挙げられています.一方で,全てのprocedureが外部procedureになるので,functionを利用するにはexternal
宣言が必要になること,procedure同士の関係が不明瞭になること,ファイルの整理が困難になることが欠点です.
実際は,内容を差し替える際に上書きされることはなく,ファイルのバックアップが作られます.また,先述の例のように,変数U
とU2
があるように,かなり行き当たりばったりで変数やprocedureが増えるので,UMAG2
やUMAG0
などが作られ,それら全てが一つのフォルダにが置かれるなど,かえって取扱いが困難になっているのが実情ですし,それを防止することはできません.1ファイル1 procedureは明らかにアンチパターンです.
エディタに依存する話ですが,VSCodeで関数doublify
の定義へ移動しようとしても,external
宣言(integer,external :: doublify
)までしか移動できず,実装に辿り着けないので,そういった意味でも好ましくない存在です.
この状況を改善するには,submodule
を使います.関係した処理をまとめて記述・管理するためにmodule
が導入されています.しかし,module
では,その中の変数やprocedureを変更すると全てコンパイルし直しになっていました.submodule
は,module
内のprocedureを個別のファイルに切り出すことを許可します.関係するモジュールをまとめてフォルダへ格納し,その中でprocedureをサブモジュールとしてsubmodule
に切り出すのです.procedureのinterface
を記述する必要性は生じますが,それによって変更した際の再コンパイル時間を短くするという利点を残しつつ,ファイル管理が煩雑化する欠点を解決できます.
program main
use :: mod_util,only: hello, doublify
implicit none
call hello()
print *,doublify(2)
end program main
module mod_util
implicit none
interface
module subroutine hello()
end subroutine hello
module function doublify(i) result(i2)
integer,intent(in) :: i
integer :: i2
end function doublify
end interface
end module mod_util
submodule(mod_util) mod_util_hello
contains
! module procedure hello ! <- functionかsubroutineかはinterfaceで記述済みなので,区別せずにmodule procedureとも書ける
subroutine hello()
implicit none
print *,"hello"
end subroutine hello
! end procedure hello
end submodule mod_util_hello
submodule(mod_util) mod_util_doublify
contains
! module procedure doublify
function doublify(i) result(i2)
implicit none
integer :: i
integer :: i2
i2 = i*2
end function doublify
! end procedure doublify
end submodule mod_util_doublify
新しくモジュールmod_util
を導入し,hello
とdoublify
をmod_util
に含めました.hello
とdoublify
をsubmodule
に含めるには,それぞれ3行(submodul...
, contains
, end submodule...
)をコピペするだけで終わります.それぞれのinterface
はmod_util
に書いてあるので,function doublify(i) result(i2)
ではなく,module procedure doublify
のように簡略化することもできます.
mod_util
はあくまで例です.関連のあるprocedureや定数をmodule
としてまとめ,それらをフォルダに分けて整理することを意識してください.
短く,意図の読み取れない変数名
これは単純に古い規格の影響です.英単語の(母音を抜いた)省略形で解読が非常に困難です.変数名と物理量の対応が書かれたエクセルが存在する場合もありますが,更新忘れが頻発しており役に立ちません.
書き換える以外に改善することはできないのですが,いきなり書き換えると影響範囲が大きく,また暗黙の型宣言を使っている場合にはコンパイルエラーから参照箇所を確認するという小技も使えないので,procedure単位で書き換えます.書き換えの際にいくつか機能を利用できます.
別名参照は,別モジュールに存在している変数を,異なる名前で利用する機能です.use モジュール名, 別名=>基の変数名
とします.
subroutine UMAG(U, U2)
use param, only: N, UsualComputation=>JQWIOD, K_ON ! JQWIODを別名で参照
implicit none
real*4,intent(in) :: U(3,N), U2(3,N)
real*4 :: mag
if(UsualComputation == K_ON)then
mag = computeVelocityMagnitute(U)
else
mag = computeVelocityMagnitute(U2)
end if
! 以下省略
end
ポインタやenum
を利用することもできます.ONやOFFは論理型で判断すべきでしょうが,ここでは一度enum
をはさんでいます.
module param
use,intrinsic :: iso_c_binding
implicit none
integer,parameter :: N = 100
real*4,target :: u(3,N) ! target属性を付けてポインタ変数と結合できるようにする
real*4,target :: u2(3,N) !
! 連番はenumに置き換える
enum, bind(c)
enumerator :: Disable=0
enumerator :: Enable
end enum
integer*1 :: JQWIOD = Enable
end module param
subroutine UMAG(U, U2)
use param, only: N, UsualComputation=>JQWIOD, Disable
implicit none
real*4,intent(in),target :: U(3,N), U2(3,N)
real*4,pointer,dimension(:,:),contiguous :: velocity ! 2次元配列へのポインタで,メモリ配置はすべて連続
real*4 :: mag
! 処理の呼出で分岐するのではなく,引数となる変数を条件に応じてポインタと結合する
velocity => U
if(UsualComputation == Disable) velocity => U2
! 以降の処理は分岐不要
mag = computeVelocityMagnitute(velocity)
! 以下省略
end
自在に飛び回る行番号
FortranコードになくてFORTRANコードにあるのは,行番号です.かつては行番号が多く使われ,FORMAT文による入出力書式の統一,入出力エラーの簡潔な処理が実現されてきました.また.DO文を適当に書けるという(書き手からすると)利点もありました.しかし,必然的にGOTO
が現れて処理の追跡が難しくなるといった問題が,特に適当に書かれたDO文で多く発生しました.また,行番号に規則性がなく,ラベルで置き換えられないのも,嫌われる一因でしょう.
Fortranでは,行番号を使わずにプログラムを書くことができます.エラー処理でgoto
(それに付随する行番号)を使うかどうかは,設計の問題なのでここでは立ち入りません.
FORMAT文を参照するための行番号は,書式を文字列として取り扱うことで回避できます.書式を文字列定数として適切な場所に置いておけば,必要なprocedureからアクセスできます.
X=10D0
Y=20D0
WRITE(*,100) X,Y
100 FORMAT(E10.1,E10.1)
character(:),allocatable :: fmt
real(8) :: x=10d0, y=20d0
fmt = '(A,E10.1,A,E10.1,A)'
print fmt, "position = (", x, ",", y, ")"
多重DO文の脱出には,名前付きdo構文とexitを利用します.反復回数を決めない汎用DO文はdo while
で置き換えられますが,汎用DO文が出てきたときは,そもそもプログラムを組む側が処理を理解していないことが多いので,設計の見直しが必要です.
DO 20
DO 20 I=1,2
DO 20 II=1,2
:
:
ERR = ...
IF(ERR < 1D-9) GOTO 100
20 CONTINUE
100 CONTINUE
real(8) :: error
integer :: i,j
error=huge(0d0)
Convergence : do while(error > 1d-9) ! 反復回数の上限を定める場合は do iteration = 1, MaxNumIterationのようにする
do j=1,N
do i=1,N
do ...
:
:
error = ...
if(error < 1d-9) exit Convergence ! 多重ループも一発で抜けられる
end do
end do
end do
end do Convergence
do while
文を使うときは,最初の判定を通すために値を設定しなければならないので少々厄介です.
入出力エラーはiostat
指定子の値に基づいて処理します.
OPEN(UNIT=5,FILE="DATA.TXT")
DO 200 I=1,10000
READ(5,*,END=210) E
200 CONTINUE
210 CONTINUE
DataRead : do i = 1, 10000
read(InputFileUnit,*, iostat = IOStatus, iomsg = IOMessage) e
if(IOStatus > 0) then
print *,IOStatus,trim(IOMessage) ! エラーメッセージはIOMessageに格納されている
exit DataRead
end if
end do DataRead
その他
上記の方法を使わずに局所的にコードを書き換える場合は,block
構文を使って影響範囲を限定してください.FORTRAN/Fortranのプログラム単位は,変数の宣言を行う宣言部と,処理を書く実行部に分かれています.宣言部は実行部より先に置かれ,変数宣言は必ず宣言部で行わなければならなかったので,何かコードを修正する際にいちいちprocedureの最上部まで戻るのが面倒でした.それを理由に暗黙の型宣言を利用する人もいましたが,block
構文が導入され,スコープを限定した局所変数を宣言できるようになりました.block
構文の中で宣言した変数の寿命は当該のblock
構文の中だけです.block
構文の外で宣言した変数の参照も可能ですし,それらと同じ名前の局所変数を宣言して,block
構文に外に影響を及ぼさずに処理することもできます.
integer :: i = 1, j = 2
print *,i,j ! 1 2
block
integer :: j ! block内変数jを新たに宣言(block外のjとは異なる)
j = 1 ! block内のjに値を代入
i = 2 ! block外変数iを参照できる
print *, i, j ! 2 1
end block
print *, i, j ! 2 2
まとめ
FORTRANコードを書き直す際に役立つと思われるFortranの機能を紹介しました.これらを用いる事で,影響範囲を限定しながら,局所的にコードを置き換えていくことができます.これらの機能を利用できないほど古いコンパイラの利用を強制されている場合は,逃げた方が賢明です.
本記事では機能を紹介しましたが,最も重要なことは,自分以外のチームメンバーの意識改革です.改革というと大げさに感じますが,まずは,心の奥底ではなんとなく修正が大変だと思っているその意識を自覚してもらうところから始めなければなりません.FORTRANのプログラムが最高に書きやすくて保守管理しやすいと思っている人は,いないんじゃないかと思っています.
イソップ寓話の北風と太陽は,非常によい教訓を教えてくれます.これはダメだと大上段から否定されて,「なるほどダメなのか.では今日から生まれ変わります」となる人はいません.こちらが強い言葉で否定すればするほど,相手は耳も心も閉ざします.著者も一時期そのように強気に出ていましたが,FORTRANプログラムやそれらを書いている人を非難したいわけではなく,保守管理しやすい環境を作り,効率よく業務を進めることが目的でしたので,太陽作戦に切り替えています.今ではかなり耳を傾けてもらえ,自身の業務の責任範囲では効率的にやらせてもらっています.ここで成功例を作り,取り組みをチーム全体に広げるのが当面の目的です.
新しい言語が好きな方は,数値計算に適した開発スタイルの開拓に注力してください.そして,その成果を自慢してください.まだFortranで疲弊してるの?と.著者はそこから新たに学ぶことができます.