AMD系インスタンスにNvidiA HPC SDKを入れて色々やってみる話②【Fortran編】

はじめに

 メリークリスマス!皆さんクリスマスいかがお過ごしでしょうか!することがない?それじゃあFortranですね!プレゼントを用意していない?Fortranの実行環境を整備してあげれば大喜び間違いなしです!楽しいクリスマスにしましょう!

 前回の記事では、Nvidia HPC SDKを導入して、(旧)PGI系のコンパイラ群を使用可能にしました。今回は、前回ちらっと予告した通り、実際にFortranのコードを書いて実行してみましょう。

前提

この記事は下記の条件を満たした方を対象としております。

  • HGAでインスタンスを作成済であること(インスタンスタイプはamd1dl, amd2dl, amd3dlのいずれか)
  • インスタンスにログイン可能であること(方法, VSCodeでの接続も推奨します)
  • 前回の記事通りツールの導入と動作確認が済んでいること
  • 何かしらのプログラミング言語使用経験あるのが望ましいです

Fortranの実行方法

 まず、最初に説明しなければならないのは、Fortranの実行方法です。実は、Fortranはコンパイル型というものに分類される言語のひとつで(他にC等があります)、コードを書いたら、そのコードにコンパイルと呼ばれる処理を施す必要があります。PythonやRuby等の言語(これらをインタプリタ型言語と呼びます)に親しまれた方は最初戸惑うかもしれません(コード書いて即python [ファイル名].pyみたいな実行はできないということです)。

 論より証拠ということでまずは以下のコードをhello.f90というファイル名で保存してください:

program HelloWorld
    ! '!'マークでコメントが書けます。「Hello, World!」はやっぱりやめました。滑った?
    print *, 'Merry Christmas!' 
end program HelloWorld

 これを保存したら、以下のコマンドでコンパイルという操作をします:

nvfortran -o hello hello.f90 

 実行したらhelloというファイルができていることが確認できると思います。これは実行ファイルと呼ばれるもので、以下のように、コードを実行することができます。

./hello
helloの実行

実行時上記のような表示になれば成功です。

 なぜこのような七面倒くさい処理が必要なのかというと、それはコードはCPU(そしてメモリ・GPU等)には理解できないため、CPU等にもわかる言葉に翻訳する必要があるからです(このような言語をマシン語といいます。究極的には0と1のみからなるビット列になります)。

 上記の実行方法は何度も繰り返して慣れるようにしてください。あなたなら大丈夫です! それでは、Fortranの基本文法について紹介していきます。

Fortranの文法

 以下ではFortranの文法について解説します。解説箇所は、変数・配列・ループ(do)・条件分岐(if)・関数・その他(Print文・キーボード入力・慣習など)に絞らせていただきます。

慣習:プログラム(ルーチン)には名前を付けるようにしましょう!

 最初に、Fortranの重要な慣習について説明させていただきます。最初に例としてあげた

program HelloWorld
    ! '!'マークでコメントが書けます。「Hello, World!」はやっぱりやめました。滑った?
    print *, 'Merry Christmas!' 
end program HelloWorld

というコードですが、実はこちらは1行目(program HelloWorld)と4行目(end program HelloWorld)を抜かしても動作はします(試してみましょう)。これらはプログラムのタイトル( HelloWorld )を表します(また、プログラムの最後にもend program HelloWorldのようにend~の形でそのプログラムの終わりを明示する必要があります。)。

 これはあくまで慣習として必要なものなので必ずしも書く必要はありませんが、後々関数やサブルーチン(本記事では解説しませんが引数や戻り値なしの関数という認識でも大丈夫だと思います)を書いた際、コード全体の見通しがよくなるので書くことを推奨します。なお、プログラムのことを正式には(メイン)ルーチンと呼びます。

print文とread

 print文についてはprint *, 'Merry Christmas!'のように謎の*,が入っていますが、これは出力書式をコンパイルのデフォルトに任すよ~という意味です(Pythonのprint文にも出力形式を指定する.format()メソッドがありますね)。

 コードの実行時にキーボード入力を受け付ける際はread関数を用います。以下のコードをinput.f90というファイル名で保存します。

! 2つの数字を受け取ってそのまま出力
program input
    implicit none  ! ルーチン内で変数を用いる際の「おまじない」です
    integer n, m  ! 変数を定義します。解説は次節で

    ! n, mの入力をキーボードから受け付ける。整数
    read *, n, m

    print *, n, m
end program input

保存が完了したら

  1. コンパイル:nvfortran -o input input.f90
  2. 実行:./input

すると下図のような画面が現れるので、半角数字を入力してEnterキーを押下、更に半角数字を入力してEnterキーを押下すると、入力した2つの数字が並んで表示されます。

inputの実行

変数

 変数を使うには以下の2ステップを踏みます。

  1. 型を明示した上で宣言する(こんな変数をint型で使うよ~みたいな)
  2. (例外もあるが)変数の初期化を行う(最初に値を入れること)

FortranはC同様静的型付言語というものに分類されまして、Python(動的型付言語と呼ばれます)と異なり変数の型(この変数には文字/整数が入るんだよ~という宣言のことです)を明示する必要があります。

前節のキーボード入力を受け付ける処理:

! 2つの数字を受け取ってそのまま出力
program input
    implicit none  ! ルーチン内で変数を用いる際の「おまじない」です
    integer n, m  ! 変数を定義します。解説は次節で

    ! n, mの入力をキーボードから受け付ける。整数
    read *, n, m

    print *, n, m
end program input

の中にinteger n, mとinteger(整数)型の変数n, mをこれから用いるよ~と宣言しています。

では、read *, n, mという箇所を

n = 5
m = 6

と置き換えて保存し

  1. コンパイル:nvfortran -o input input.f90
  2. 実行:./input

上記を順に実行すると画面に5と6がそれぞれ表示されます。実は先ほどキーボード入力受付処理は、キーボードで入力した内容を変数に代入することによって、変数の初期化を行っていたんですね。

 Python等インタプリタ型言語から入った方(筆者もです)は「なんで最初にわざわざ変数にタイプなんて指定する必要あるの??」と疑問に思われるかもしれません。実はPythonも実行時には必ず変数の型を特定しています。 これは究極的にはCPUとメモリとコードの関係が織りなす一連のストーリー(それはGPUとVRAMとコードの関係にもよく似ています)につながるのですが、話しているとこの記事が千夜一夜物語になりそうなので割愛します。でもいつか語りたいですね~(特にGPUとVRAMの関係について)。

配列

 配列は以下のように宣言します。

[型名(integer等)] 配列名(0:9)

以下に配列を使った例を示すので、コメント文を参考に基本的な配列処理を学びましょう:

program ArrayTest
    implicit none
    integer a(0:9)  ! 配列の宣言の際必要なのは型と配列長さです 

    print *, a  ! 配列はそのままprint文で出力できます

    a = (/1,2,3,4,5,6,7,8,32767,0/)  ! //で囲んで代入可能です
    print *, a

    a(4) = 14  ! indexを指定して置き換えることも可能です
    print *, a

    a(5:6) = (/15, 16/)  ! indexの範囲を指定して置き換えることも可能です
    print *, a

    a(7:9) = a(7:9) + 1  ! 配列の全要素に1が加算されます
    print *, a
    
    a = -a  ! 配列の全要素の符号を反転させます
    print *, a
end program ArrayTest

上記を array.f90というファイル名で保存し

  1. コンパイル:nvfortran -o arrayarray.f90
  2. 実行:./array

で実行した際、下図のような出力が成されれば成功です。

なお、print文で出力formatをコンパイラのデフォルトにしたために、図に示すように配列が途中で勝手に開業されて出力されています。気になる

arrayの実行

条件分岐(if文)

 if文は次の構文で定義されます。

if (条件式) then
    (条件式がtrueとなるときの処理)
else
    (条件式がfalseとなるときの処理)
end if

 Pythonであれなんであれ、if文使った経験があれば特に困らないと思います。thenend ifの付け忘れにご注意ください。またif文を用いたサンプルコードを以下に示します。

program MerryChristmas
    implicit none
    integer flag
    character(len=52) str

    print *, 'クリスマスの予定の有無を1/0で入力してください(1: 予定あり, 0: 予定なし)。'

    read *, flag

    if (flag == 0) then
        str = '筆者といっしょですね!'
    else
        str = '楽しんで下さいね!(血涙)'
    end if
    
    print *, str
    
end program MerryChristmas

上記をchristmas.f90という名前で保存したら「Fortranの実行方法」節を参考に実行してみましょう。メリークリスマス!

反復処理(ループ文)

 基本的なループ処理は以下の構文で実装されます。

  do 変数=初期値,最終値[,刻み幅]
    (繰り返したい処理)
  end do

これを参考に次のサンプルコードをdo.f90というファイル名で保存し実行してみましょう:

program Do
    do i=0,9
        print *, i, 'ブログ書くの疲れた'
    end do
end program Do

 注意点としては、最終値も含まれて実行される点です。Pythonのfor文でrangeで指定している方は最初戸惑うかもしれません(筆者もそうでした)。

関数

 関数の定義は以下の構文で行えます:

function [関数名] (仮引数)
    (一連の処理)
  (戻り値は最後に書いた変数など)
end function

 それではサンプルとしてキーボードで入力した2つの値間の最大公約数を求めるアルゴリズム(ユークリッド互除法)を関数化しましょう。なお、Fortranでは(バージョンによりますが)再帰的呼出を行うことも可能です。

program CallingGcdFunction
    implicit none
    integer n, m, getgcd, inputed_m, inputed_n

    read *, n, m
    inputed_n = n
    inputed_m = m
    
    print *, inputed_n, inputed_m, getgcd(n, m) ! 関数の呼び出しは実引数をセットするだけでおーけー!
end program CallingGcdFunction

! 以下では2数間の最大公約数を求める関数getgcdを定義します。
! 引数は2つinteger型です。
function getgcd (a, b)
    implicit none
    integer getgcd, a, b, bx

    do while (a > 0)
        bx = a
        a = mod(b, a)
        b = bx
    end do

    getgcd = b
end function getgcd

 上記をgcd_func.f90というファイル名で保存し、「Fortranの実行方法」節を参考にコンパイルと実行を行うと下記のような画面が出ます。実行したら、演算対象としたいinteger型の数値を一つ入力してEnterキーを押下し、更にもう一方の数字を入力しEnterキーを押下することで、下図右下に結果が表示されます(2214と77760間のGCDは54という意味です)。

gcd_funcの実行

 クリスマスシーズンなので便乗しますが、ユークリッド互除法は私たちに、パートナ同士がお互い割り切れない部分を無くしていくことの重要性を教えてくれているんだと思います(ちなみに筆者はこの箇所をドヤ顔で書いています)!メリークリスマス!

次回予告:OpenACC

 ここまで終えた方、本当にお疲れさまでした!Fortranを触れてみていかがでしたでしょうか?コンパイル型であれインタプリタ型であれ、何かしらのプログラミング言語を経験されたことのある方なら、文法自体は比較的馴染みよかったのではないでしょうか?これが70年近くの歴史を持つ言語と知った時、筆者はびっくりしました。バージョンごとにモダン化していった流れはあるのですが、コアの部分は今のよく使われている言語と大して違いがないからです。理数系専攻の大学生の方なら、今でも「~の研究室ではFortran」を使っていると耳にすることもあるかもしれません。古いながら、今でも生きてる言語です。

 さて、次回(恐らく来年1月上旬頃になります)はFortranのコードを並列化してみようと思います。

 その前にまず、以下の画像をご覧になっていただきたいのですが、これはOpenACCを導入せずコンパイルした場合と導入してコンパイルした場合とで、実行時間をそれぞれ比較したものです(2214と77760のGCDを求めるコードです)。

上の方がOpenACCなしでコンパイルして実行した結果で、下の方は有りでコンパイル・実行した結果です。

 両者を見比べると、OpenACCを導入した場合、処理時間がかえって長くなっていることがわかります。これは、そもそもOpenACCを動作させるための処理分が加算された上、並列化処理によるメリットが得られないほどシンプルなループを使ったためです。並列化のメリットは、回数が膨大だったりネストが入り組んだループ箇所以外では、得るのが難しいんですね。

OpenACCで高速化!・・・ってあれ?

 よって次回は検証用としてもう少し入り組んだ処理で検証したいと思います。その為にも、Fortranの知識は前提なので(CでもC++でも良いですが)、今回の記事の内容はよく復習されることをおすすめします。

筆者もコードの準備と復習頑張ります。どうせ暇なクリスマスですしね。メリークリスマス!

[HGAoffcial]

[dot]

[/dot]

この記事をシェアする