読者です 読者をやめる 読者になる 読者になる

Fortranでバイナリを読み書きするときのあれこれ

Fortranで何らかの数値をファイルに記録する際、I/Oの高速化、ファイルサイズの制限といった理由からテキストではなくバイナリで出力したい場合がある。規模の大きいデータを扱う場合やシミュレーションの分野など。その読み書きについての記事。

だいたい、Fortran95 Standardコンパイラのドキュメントのまとめ。日本語訳では無いので注意(間違いが含まれていても責任は取れません。ごめんなさい)。

環境

CPUはリトルエンディアン
gfortran(gcc) 6.3.1
基本的にFortran95で記述

OPEN文の指定子について

OPEN文の指定子(specifier)のうち、バイナリ読み書きについてのみ抜粋。

  • FORM= : FORMATTED or UNFORMATTED
    FORMATTEDはテキスト, UNFORMATTEDはバイナリ の読み書きになる。
    デフォルト値は、DIRECT ACCESSの場合はUNFORMATTED、SEQUENTIAL ACCESSの場合はFORMATTED。

  • ACCESS= : SEQUENTIAL(デフォルト) or DIRECT
    SEQUENTIALでは前後にレコード長(record marker)が付加される。DIRECTではされない。レコード長の指定方法にも違いがある。具体例は後ほど。
    (なお、これに加えてFortran2003ではSTREAMが追加された。そのことも後ほど。)

  • RECL= : 正の整数.
    DIRECT ACCESSの場合、各レコードの長さの指定。必ず指定しなければならない。
    SEQUENTIAL ACCESSの場合、レコードの最大の長さの指定。指定しないとコンパイラ依存。GFortranの場合、1073741824 bytes (1 GB)になる模様*1。いずれの場合もGFortranならbyte単位。

これらに加えて、GFortranでは独自拡張で CONVERT指定子も利用できる*2エンディアンを指定。
NATIVE(デフォルト), SWAP(相互変換), LITTLE_ENDIAN, BIG_ENDIAN から指定。

コマンドラインオプション(GFortran)

参考 : https://gcc.gnu.org/onlinedocs/gcc-6.3.0/gfortran/Runtime-Options.html

  • -frecord-marker : 4(デフォルト) or 8
    record markerの長さをbyte単位で指定。昔のGFortranではデフォルトが8だったらしく、互換性のために明示的に指定する必要がある場合もある。

以下のサンプルコードでは、これらのオプションを使用せずに(つまり、デフォルト値のままで) コンパイルしている。

SEQUENTIAL ACCESSの例

program main
implicit none
integer :: i
real, parameter :: buf = 3.14

open(10, file='testf.bin', form='unformatted', status='replace')
write(10) buf
close(10)

end program main

xxd(vimに付属)でダンプしてみる

$ xxd testf.bin
00000000: 0400 0000 c3f5 4840 0400 0000            ......H@....

前後にrecord markerがそれぞれ4byte分、byte単位で追加されている(realは単精度浮動小数点数で4byte. リトルエンディアンな環境であることに注意)


データを増やしてみる。

program main
implicit none
real, parameter :: buf(5) = (/3.14, 5.28, 4.76, 0.32, 0.04/)

open(10, file='testf2.bin', form='unformatted', status='replace')
write(10) buf
close(10)

end program main
$ xxd testf2.bin
00000000: 1400 0000 c3f5 4840 c3f5 a840 ec51 9840  ......H@...@.Q.@
00000010: 0ad7 a33e 0ad7 233d 1400 0000            ...>..#=....

1レコードが4byte*5=20byteだから、record markerは0x14


ところで、こんな書き方も思いつくと思う。

program main
implicit none
integer :: i
real, parameter :: buf(5) = (/3.14, 5.28, 4.76, 0.32, 0.04/)

open(10, file='testf3.bin', form='unformatted', status='replace')
do i=1, 5
    write(10) buf(i)
end do
close(10)

end program main
$ xxd testf3.bin
00000000: 0400 0000 c3f5 4840 0400 0000 0400 0000  ......H@........
00000010: c3f5 a840 0400 0000 0400 0000 ec51 9840  ...@.........Q.@
00000020: 0400 0000 0400 0000 0ad7 a33e 0400 0000  ...........>....
00000030: 0400 0000 0ad7 233d 0400 0000            ......#=....

WRITE文1つにつき1対のrecord markerが挿入される、ということ。
record markerが多いのでもったいない気がしてくる。


読み出しの例。
形式さえ分かっていれば指定されただけメモリを確保しておけば読める。

2番目の例ならこうなる。

program main
implicit none
real :: buf(5)

open(10, file='testf2.bin', form='unformatted', status='old')
read(10) buf
close(10)
write(*,*) buf

end program main
./a.out
   3.14000010       5.28000021       4.76000023      0.319999993       3.99999991E-02

DIRECT ACCESSの場合

まずはサンプルコード。

program main
implicit none
real, parameter :: buf(5) = (/3.14, 5.28, 4.76, 0.32, 0.04/)

open(10, file='testfd.bin', access='direct', recl=4*5, status='replace')
write(10, rec=1) buf
close(10)

end program main
$ xxd testfd.bin
00000000: c3f5 4840 c3f5 a840 ec51 9840 0ad7 a33e  ..H@...@.Q.@...>
00000010: 0ad7 233d                                ..#=

write文のREC=指定子でレコード番号を指定している。
testf2.binと比較すると、前後のrecord markerが無いことが確認できる。


あるいは、

program main
implicit none
integer :: i
real, parameter :: buf(5) = (/3.14, 5.28, 4.76, 0.32, 0.04/)

open(10, file='testfd.bin', access='direct', recl=4, status='replace')
do i=1,5
    write(10, rec=i) buf(i)
end do
close(10)

end program main

レコード長を変えただけ。"各"レコードの長さを指定するため、recl=4 とする。


読み込みも一応置いておくだけ。

program main
implicit none
integer :: i
real :: buf(5)

open(10, file='testfd.bin', access='direct', recl=4*5, status='old')
read(10, rec=1) buf
close(10)
write(*,*) buf

end program main
$ ./a.out
   3.14000010       5.28000021       4.76000023      0.319999993       3.99999991E-02

DIRECT ACCESSではrecord markerが無いので、書き出す側のデータ型と順序さえ知っていれば、読み出す側はrecordの区切りを自由に決められる。
ただ、レコード長をOPENの際に決めなければならないので、複数のデータ型がある場合にはWRITE文1つで一気に読み出すか最も大きいデータ型に合わせるかになる。

ストリーム入出力の話

ところで、C言語で書いてみるとこんな感じになると思う。(エラー処理なんてなかった)

#include <stdio.h>

int main(){
    const float buf = 3.14;
    FILE *fp = fopen("testc.bin", "wb");
    fwrite(&buf, sizeof(buf), 1, fp);
    fclose(fp);

    return 0;
}
$ xxd testc.bin
00000000: c3f5 4840                                ..H@

このように、データ長をファイルopen時ではなく書き出し時に指定する。
Fortran2003からは ACCESS=指定子が新たに STREAM を取れるようになり、同じことが出来るようになった。

program main
implicit none
real, parameter :: a=3.14
double precision :: b=0.04d0
character(len=3) :: buf='foo'

open(10, file='testfs.bin', access='stream', status='replace')
write(10) a, buf, b
close(10)

end program main
$ xxd testfs.bin
00000000: c3f5 4840 666f 6f7b 14ae 47e1 7aa4 3f    ..H@foo{..G.z.?

4byte, 3byte, 8byte の順。
書き込むデータの方に合わせて書き込み先でのデータ長が定まる。


まとめ

Fortranでバイナリデータの入出力を扱う場合には

  • エンディアン
  • record markerの有無とその長さ
  • データ型とその長さ、構成(配列要素数と並び方)

に注意。
Fortran2003以降を使えるなら使ったほうが楽だよ。

その他参考

Fortranの各言語仕様などはここを見るとよいと思う。
Fortran 90 — Fortran90 1.0 documentation
Fortranによるバイナリファイルの読み書きについての質問とその答え
binaryfiles - Fortran unformatted file format - Stack Overflow