Fortranにおける実行時間の測定

# 概要
Fortranでプログラムの実行時間を測定する方法とそれに用いるサブルーチン/関数についてまとめました．

## 使用環境
|コンパイラ|バージョン|
|:--|:--|
|intel Parallel Studio XE Composer Edition for Fortran|17.0.4.210|
|PGI Visual Fortran for Windows|18.7|
|gfortran|7.3.0 (for Ubuntu on WSL)|

## 更新履歴
- 2018年12月24日 [補足](#補足連続メモリアクセスする場合)を追記
- 2018年12月25日 誤字修正

# 時間測定を行うサブルーチン

Fortranには，実行時間の測定に利用できるサブルーチンが三つ用意されています．

|サブルーチン|機能|
|:--|:--|
|`system_clock([count=現在のカウント, count_rate=1秒あたりのカウント, count_max=カウントの最大値)`|ある基準時点からの経過時間をカウントという単位（整数）で取得する<br>`count_rate`引数に整数型変数を渡すと1秒あたりのカウント数が得られる<br>`count_max`はカウントの最大値であり，これを越えるとカウントが0に戻る|
|`cpu_time(time=経過秒数)`|ある基準時点からのCPU時間を取得する|
|`date_and_time([date=年月日, time=時分秒ミリ秒, zone=時差, values=年月日時差時分秒ミリ秒])`|サブルーチン呼出し時点の年月日と時刻（時，分，秒，ミリ秒）取得する<br>`date`はyyyymmdd形式の**文字列**<br>`time`はhhmmss.fff形式の**文字列**<br>`zone`はshhmm形式の標準時からの時差の**文字列**<br>`value`は要素数8の整数型配列で，それぞれ年, 日, 時, 差時（分）, 分, 秒, ミリ秒を持つ|

もう一つ，Fortranコンパイラであれば大抵は使える関数があるので，その関数を使った測定方法を後ほど紹介します．

## sytem_clock

サブルーチン`system_clock()`を用いると，ある基準時点からの経過時間をカウントという単位（整数）が得られます．1秒間に何回カウントされるかも取得できます．`system_clock()`を用いてプログラムの実行時間を測定するには，以下の手順と実装が考えられます．

1. 測定したい処理を実行する直前で`system_clock()`を呼ぶ．このとき，1秒あたりのカウント数も取得しておく．
1. 測定したい処理が終了したら`system_clock()`を呼ぶ．
1. 終了後のカウントと実行直前のカウントの差を計算し，1秒あたりのカウント数で割って秒に変換する．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    implicit none

    integer(int32) :: time_begin_c,time_end_c, CountPerSec, CountMax
    
    call system_clock(time_begin_c, CountPerSec, CountMax)
    call sleep(1)
    call system_clock(time_end_c)

    print *,time_begin_c,time_end_c, CountPerSec,CountMax
    print *,real(time_end_c - time_begin_c)/CountPerSec,"sec"
end program main
```

時間の測定に用いる変数は，末尾に`_c`として単位を付けています．処理の開始/終了をstart/stopと書くべきか，begin/endと書くべきかの議論は本書の対象ではありません．
`sleep(秒数)`は，引数で与えた秒数だけプログラムの実行を停止します．実数を与えてしまうと，整数に変換されるわけでもなく異様に長い時間停止するので注意が必要です．

各コンパイラでの実行結果は以下のようになりました．

```console
Intel
   831208950   831219030       10000  2147483647
   1.008000     sec

PGI
            0      1010999      1000000   2147483647
    1.010999     sec

gfortran
    12245274    12246275        1000  2147483647
   1.00100005     sec
```

この測定方法はいくつか注意点があります．

- 上の実行例からもわかりますが，調査した範囲では1秒あたりのカウント数は全てのコンパイラで異なるので，必ず`system_clock()`で取得する必要があります．
- 引数に与える整数型変数の種別によって得られる値が変化します．
- カウントは上限値`count_max`を超えると0に戻るので，処理の実行中に上限値を越えてしまうと経過時間が負になるなど，正しい値が得られません．この対策は，`integer(int64)`を用いることです．
- 上に関連して，基準時点は全てのコンパイラで異なります．

つまり，**`system_clock()`で得られる数値（カウント，1秒あたりのカウント）の可搬性は皆無である**ことを把握しておきましょう．

### 1秒あたりのカウント数
`system_clock()`で利用可能な整数型の種別と，そのときに得られる各種パラメータの値を表にまとめました．

|整数型の種別|Count Rate [clock/s]|Count Max [count]|測定上限|備考|
|:--|:--|:--|:--|:--|
|int16|1000|32767|約33秒| Intelコンパイラのみ利用可能[^int16pgi][^int16g]|
|int32|10000[^intel]<br>1000000[^pgi]<br>1000[^g]|2147483647|約2.5日[^intel]<br>約36分[^pgi]<br>約25日[^g]||
|int64|1000000[^intel]<br>10000000[^pgi]<br>1000000000[^g]|9223372036854775807|約290000年[^intel]<br>約29000年[^pgi]<br>約290年[^g]||

[^intel]: Intelコンパイラ
[^pgi]: PGIコンパイラ
[^g]: gfortran
[^int16pgi]: PGIコンパイラではCount Rate, Count Maxを取得できるが測定できない．
[^int16g]: gfortranは完全に非対応．カウントは`-huge(0_int16)`となり，`count_rate`, `count_max`共に0となる．

Intelコンパイラで`integer(int16)`を用いて`sleep(30)`の実行時間を測定したところ，途中でカウントが上限値32767を越えて0に戻り，実行時間が負になりました．

```console
  4961   2977   1000  32767
  -1.984000     sec
```
この問題を回避するために，**`system_clock()`で実行時間を測定するときは`integer(int64)`を使うようにします**．さすがに290年連続で動かすことはないでしょう．

1カウントが最小の時間測定の単位と考えられますが，Windowsプラットフォームではマイクロ秒単位での測定はできていないようです．

### system_clockの基準時点
`system_clock()`はある基準時点からの経過時間をカウントという単位で返します．その基準時点もコンパイラによって異なります．

|コンパイラ|基準時点|
|:--|:--|
|Intel   |協定世界時（UTC）の1970年1月1日の00:00|
|PGI     |1回目の`system_clock`が実行された時点<br>1回目の`system_clock()`のカウントは0|
|gfortran|OS(Ubuntu on WSL)の起動時|

上で示した実行結果を再掲しますが，PGIコンパイラは1回目のカウントが0になっていることがわかります．

```console
PGI
            0      1010999      1000000   2147483647
    1.010999     sec
```

Intelコンパイラでコンパイルしたプログラムを2018年12月22日午前1時30分に実行すると，

```console
      1545410046972000      1545410047974000               1000000   9223372036854775807
   1.002000     sec
```
という結果が得られました．1545410046972000カウントを1000000 count/sで除して単位を換算すると，約49年という値が得られました．

## cpu_time
サブルーチン`cpu_time()`を用いると，ある基準時点からのCPU時間が秒の単位で得られます．秒とはいっても，引数として渡すのは実数なので，1秒未満の時間は小数点以下の値として得られます．`system_time()`と同様に，`cpu_time()`を用いて以下の手順で実行時間を測定します．

1. 測定したい処理を実行する直前で`cpu_time()`を呼ぶ．
1. 測定したい処理が終了したら`cpu_time()`を呼ぶ．
1. 終了後の時間と実行直前の時間の差を計算する．

秒単位で取得できるので，とても楽です．テストプログラムを下のように実装しました．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    implicit none

    real(real32)   :: time_begin_s,time_end_s

    call cpu_time(time_begin_s)
    call sleep(1)
    call cpu_time(time_end_s)

    print *,time_begin_s, time_end_s
    print *,time_end_s - time_begin_s,"sec"
end program main
```

時間の測定に用いる変数は，末尾に`_s`として単位を付けています．

得られた数値の換算をする必要はありませんが，実行結果を見ると，これも厄介なサブルーチンであることがわかります．

```console
Intel
  0.0000000E+00  0.0000000E+00
  0.0000000E+00 sec

PGI
    0.000000        1.005000
    1.005000     sec

gfortran
   1.56250000E-02   1.56250000E-02
   0.00000000     sec
```

Intelコンパイラとgfortranでは`sleep()`前後の時間が等しく，その結果実行時間が0になっています．`cpu_time()`はその名前の通り，CPU時間を取得します．`sleep()`はCPUを（正確にはプロセス）を停止するので，CPU時間が経過しないことが原因だと考えられます．つまり，`cpu_time()`で実行時間を測定すると，実時間（時計の経過時間）ではなく，実時間からCPUが働いていない時間（`sleep`や`read*`による標準入力待ちなど）を除いた時間が得られます．

これがよい挙動かどうかは測定したい処理によって異なるでしょう．全てのコンパイラで挙動が一致していれば問題ありませんが，調査した範囲では，Intelコンパイラとgfortranは上述の挙動ですが，PGIコンパイラは`read*`の入力待ち時間も測定しており，どうも実時間が得られるようです．

### cpu_timeの基準時点
`system_clock()`の基準時点と同様に，`cpu_time()`の基準時点はコンパイラで異なります．数値から推察すると，Intelコンパイラおよびgfortranはプログラム実行時，PGIコンパイラは1度目の`cpu_time()`を呼び出した時点と思われます[^st]．

[^st]: ただし，`cpu_time()`はプログラム実行時を基準とすることも，CPU時間が0から始まることも保証していません．

それを確認するために，`cpu_time()`を呼び出す前に時間のかかる処理を置いてみます．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    implicit none

    real(real32)   :: time_begin_s,time_end_s

    integer,parameter :: N = 2**8
    real(real32),allocatable :: a(:,:), b(:,:), c(:,:)
        
    allocate(a(N,N))
    allocate(b(N,N))
    allocate(c(N,N))

    call random_number(a)
    call random_number(b)
    c = matmul(a,b)
    print *,c

    call cpu_time(time_begin_s)
    call random_number(a)
    call random_number(b)
    c = matmul(a,b)
    print *,c
    call cpu_time(time_end_s)

    print *,time_begin_s, time_end_s
    print *,time_end_s - time_begin_s,"sec"

    deallocate(a)
    deallocate(b)
    deallocate(c)
end program main
```

行列の要素をすべて乱数で決定し，それらの行列の行列－行列積を計算し，全要素を画面に出力しました．`time_begin_s`, `time_end_s`の値が変化し，経過時間が測定できました．

```console
Intel
  7.8125000E-02  0.2031250
  0.1250000     sec
PGI
    0.000000        3.374000
    3.374000     sec

gfortran
   6.25000000E-02   9.37500000E-02
   3.12500000E-02 sec
```

## date_and_time
サブルーチン`date_and_time()`を用いると，サブルーチン呼出し時点の年月日と時刻（時，分，秒，ミリ秒）が得られます．引数を選ぶことで，欲しい情報を選んで取得できます．

とりあえず，どのような結果が得られるのかを確認します．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    implicit none

    character(8)  :: date ! yyyymmdd
    character(10) :: time ! hhmmss.fff
    character(5)  :: zone ! shhmm
    integer :: value(8)   ! yyyy mm dd diff hh mm ss fff

    call date_and_time(date, time, zone, value)
    print *,date
    print *,time
    print *,zone
    print *,value
end program main
```

```console
Intel
 20181222
 221228.368
 +0900
        2018          12          22         540          22          12          28         368

PGI
 20181222
 221234.274
 +0000
         2018           12           22            0           22           12           34          274

gfortran
 20181222
 221237.809
 +0900
        2018          12          22         540          22          12          37         809
```

`date`, `time`は数値のように見えますが，文字列です．`zone`は標準時からの時差で，Intelコンパイラおよびgfortranは協定世界時，PGIコンパイラはおそらくOSのタイムゾーンを標準時（日本の場合はJST）としています．

個別の値を数値として得るには，`values`引数に整数型配列を渡します．上の実行結果では，配列`value(8)`の各要素に整数値が得られています．時差は`zone`とは異なり，単位は分です．上の実行結果では，+9時間が540になっていることが確認されます．

`values`引数を用いる場合には，`enum`で添字に有意な名前を付けておくと利便性が向上します．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    implicit none
   
    enum, bind(c)
        enumerator :: Year = 1
        enumerator :: Month
        enumerator :: Day
        enumerator :: TimeDifference_min
        enumerator :: Hour
        enumerator :: Minute
        enumerator :: Second
        enumerator :: Millisecond
    end enum

    integer :: value(8)

    call date_and_time(values = value)
    print '(I2,A,I2)',value(Hour),":",value(Minute) ! 22:15
end program main
```

`system_clock()`および`cpu_time()`と同様に，`date_and_time()`を用いて実行前後の年月日時分秒ミリ秒を得ると，経過時間を計算できます．

1. 測定したい処理を実行する直前で`date_and_time(values=...)`を呼ぶ．
1. 測定したい処理が終了したら`date_and_time(values=...)`を呼ぶ．
1. 終了後の年月日時分秒ミリ秒と実行直前の年月日時分秒ミリ秒から経過時間を計算する．

`date_and_time()`はシステム時間を取得できるので，人間が体感する形でプログラムの実行時間を取得するのに都合がよいのですが，処理の実行前後の年月日時分秒ミリ秒から経過時間を計算する処理が必要になります．`system_clock()`や`cpu_time()`のように単純ではありません．日を跨ぐ程度なら大したことはありませんが，月を跨ぐ場合には，各月の日数が必要になります．閏年の時に2月を跨ぐ場合はどうするかなどを考え始めると，ちょっと面倒くさそうです．`values`引数で取得した配列を引き算したら，スパッと経過時間を出してくれると楽だったのですが[^dtype]．

[^dtype]: もちろん，ユーザ定義派生型`date_and_time`を定義して，演算子をオーバーロードすればそういうことも可能ですが，ユーザ定義のモジュールを使い始めると，新しくプロジェクトを作るたびにuseしないといけませんし，異なる計算機環境へのお引っ越しに手間がかかってしまいます．

# 第4の方法（OpenMPを利用する方法）
さて，Fortran標準で実行時間を測定する方法を3通り見てきました．単純に実行時間を測定したいだけなのに，どれも決め手に欠けます．`system_clock()`と`cpu_time()`はコンパイラ間の可搬性がありません．詳しくは後述しますが，プログラムを並列化して実行すると，正しく測定できません．`cpu_time()`は実時間が取得できません．`date_and_time()`は実行時間の計算が面倒です．

OpenMPの`omp_get_wtime()`関数を利用すると，実行時間を実時間で簡単に取得できます．OpenMPはFortranの標準機能ではありませんが，大抵のFortranコンパイラで利用できるはずです．

[OpenMP](https://www.openmp.org/)は共有メモリ計算機においてプログラムのマルチスレッド並列化に用いるインタフェースです．並列化したい箇所に指示句（ディレクティブ）を挿入することで，コンパイラに並列化を指示します[^para]．OpenMPに対応していないコンパイラあるいはOpenMPのディレクティブを処理しないようにすると，ディレクティブはコメント扱いになって無視されます．そのため，逐次プログラムと並列プログラムを共通のソースコードで管理できるという利点があります．

単純な`do`ループを並列化するには次のようにディレクティブを挿入します．

```Fortran
!$omp parallel do
do i = 1, N
    c(i) = a(i) + b(i)
end do
!$omp end parallel do
```
もしくは

```Fortran
!$omp parallel workshare
c = a + b
!$omp end parallel workshare
```
とも書けます．

[^para]: コンパイラの自動並列化の方が手軽ですが，自動並列化は並列化可能で，並列化による効果があるとコンパイラが判断したときしか行われません．一方，OpenMPは並列化可能か否かに関わらず，問答無用で並列化するように指示します．

コンパイラにOpenMPのディレクティブを処理させるには，オプションを付与します．

|コンパイラ|オプション|
|:--|:--|
|Intel|`/Qopenmp`|
|PGI|`-mp`|
|gfortran|`-fopenmp`|


## omp_get_wtime
紹介してきた方法は全てサブルーチンでしたが，`omp_get_wtime()`は関数です．利用するには`omp_lib`を`use`する必要があります．

|サブルーチン|機能|
|:--|:--|
|`omp_get_wtime()`|過去のある時点[^past]からの経過秒数を倍精度で返す|

[^past]: 過去のある時点は決まっていないが，実行中に変化しないことが保証されている．

下のように実装し，コンパイルオプションを付けてコンパイルすると，実行時間が得られます．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    use omp_lib
    implicit none

    real(real64)   :: time_begin_s,time_end_s

    time_begin_s = omp_get_wtime()
    call sleep(1)
    time_end_s = omp_get_wtime()

    print *,time_begin_s,time_end_s
    print *,time_end_s - time_begin_s,"sec"
end program main
```

```console
Intel
   522309.037275617        522310.055093688
   1.01781807123916      sec

PGI
    0.000000000000000         1.011729508463759
    1.011729508463759      sec

gfortran
   4556.2882710000003        4557.2884759999997
   1.0002049999993687      sec
```

ところが，コンパイルオプションを付けないと，`omp_get_wtime()`の参照エラーが生じます．OpenMPの利点は，コンパイルオプションの有無で逐次実行と並列実行を切り替えられることです．コンパイルオプションの有無でエラーが出るのは好ましくありません．

そこで，OpenMPのディレクティブを処理するオプションが付与された時にだけコンパイルされるよう，ディレクティブセンチネル[^sentinel]`!$`を導入します．

[^sentinel]: 日本語にすると，ディレクティブ標識，条件付きコンパイル標識あたりでしょうか．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    !$ use omp_lib
    implicit none

    !$ real(real64)   :: time_begin_s,time_end_s

    !$ time_begin_s = omp_get_wtime()
    call sleep(1)
    !$ time_end_s = omp_get_wtime()

    !$ print *,time_begin_s,time_end_s
    !$ print *,time_end_s - time_begin_s,"sec"
end program main
```

今度はオプションがない場合には何も表示されなくなり，オプションを付けたときのみ実行時間が表示されるようになるはずです．

## 並列プログラムの実行時間の測定
Fortranが高度なプログラミング言語かどうかは，何ができたら高度かを定義できないと議論できませんが，Fortranの利用が想定されているのは，数値計算分野で現れるでかい配列に対する並列計算です．せっかくなので，OpenMPを用いて並列計算を行うプログラムの処理時間を，各手続を用いて測定してみます．

下のプログラムでは，要素数$2^{13}\times 2^{13}$の配列`a,b,c`の足し算を並列に実行します．スレッド数は`omp_set_num_threads(スレッド数)`を利用して，1,2,4と変化させます．
実行時間の測定は，ここまでと同じように足し算を実行するループの前後で各手続を呼出し，その差を取ることで得ます．時間測定のための手続を呼び出す際，`omp master`ディレクティブを利用して，マスタースレッドのみが呼出しを行うようにしています．

```Fortran
program main
    use,intrinsic :: iso_fortran_env
    !$ use omp_lib
    implicit none

     enum, bind(c)
        enumerator :: Year = 1
        enumerator :: Month
        enumerator :: Day
        enumerator :: TimeDifference_min
        enumerator :: Hour
        enumerator :: Minute
        enumerator :: Second
        enumerator :: Millisecond
    end enum

    integer(int32),parameter :: N = 2**13
    real(real64),allocatable :: a(:,:),b(:,:),c(:,:)
    integer(int32) :: i,j

    integer(int64) :: time_begin_c,time_end_c,CountPerSec
    real(real32)   :: time_begin_s,time_end_s
    !$ real(real64)   :: time_begin_ws,time_end_ws     ! 変数名の重複を避けるため，wall-clock timeのwを追加
    integer :: time_begin_values(8),time_end_values(8) !標準種別

    allocate(a(N,N))
    allocate(b(N,N))
    allocate(c(N,N))

    ! 起動するスレッド数
    !$ call omp_set_num_threads(4)

    !$omp parallel
    ! 配列の初期化
    !$omp do
    do i = 1,N
    do j = 1,N
        a(i,j) = 1._real64
        b(i,j) = 2._real64
        c(i,j) = 0._real64
    end do
    end do
    !$omp end do

    ! system_clock
    !$omp master
    call system_clock(time_begin_c)
    !$omp end master
    !$omp do
    do i = 1,N
    do j = 1,N
        c(i,j) = a(i,j) + b(i,j)
    end do
    end do
    !$omp end do
    !$omp master
    call system_clock(time_end_c,CountPerSec)
    print *,real(time_end_c - time_begin_c)/CountPerSec,"sec",sum(c)/N/N
    !$omp end master

    ! cpu_time
    !$omp master
    call cpu_time(time_begin_s)
    !$omp end master
    !$omp do
    do i = 1,N
    do j = 1,N
        c(i,j) = a(i,j) + b(i,j)
    end do
    end do
    !$omp end do
    !$omp master
    call cpu_time(time_end_s)
    print *,time_end_s - time_begin_s,"sec",sum(c)/N/N
    !$omp end master
    
    ! date_and_time
    !$omp master
    call date_and_time(values = time_begin_values)
    !$omp end master
    !$omp do
    do i = 1,N
    do j = 1,N
        c(i,j) = a(i,j) + b(i,j)
    end do
    end do
    !$omp end do
    !$omp master
    call date_and_time(values = time_end_values)
    print *,time_end_values(     Second)-time_begin_values(     Second) &
          +(time_end_values(Millisecond)-time_begin_values(Millisecond))/1000.0,"sec",sum(c)/N/N
    !$omp end master

    ! omp_get_wtime
    !$omp master
    !$ time_begin_ws = omp_get_wtime()
    !$omp end master
    !$omp do
    do i = 1,N
    do j = 1,N
        c(i,j) = a(i,j) + b(i,j)
    end do
    end do
    !$omp end do
    !$omp master
    !$ time_end_ws = omp_get_wtime()
    !$ print *,real(time_end_ws - time_begin_ws,real32),"sec",sum(c)/N/N
    !$omp end master
    !$omp end parallel

    deallocate(a)
    deallocate(b)
    deallocate(c)
end program main
```

配列`c`の平均を画面出力しているのは，最適化によって足し算のループ自体が無視されることを避けるためです．
それぞれのコンパイラで以下のようなオプションをつけてコンパイルし，作成された実行ファイルを実行しました．

|コンパイラ|オプション|
|:--|:--|
|Intel|`/O2 /Qopenmp`|
|PGI|`-mp -fast -tp=px`|
|gfortran|`-fopenmp -Ofast`|

Intelコンパイラの最適化レベルは，他のコンパイラより1段階低くなっています．PGIコンパイラの`-tp=px`はIntel x86アーキテクチャを対象にしたコードを生成するオプションです．

**ループ順序の違いの影響は，[補足](#補足連続メモリアクセスする場合)で言及しています**．

測定結果を表に示します．各コンパイラでコンパイルした後，`date_and_time`の測定時間が負でない結果が5個得られるまで実行して得た結果から最大値と最小値をのぞき，残った3個を平均した値です．gfortranだけ著しく遅いのですが，これは間違いではありません[^workshare]．

|コンパイラ|時間測定の手続|実行時間 [s]<br>1スレッド|実行時間 [s]<br>2スレッド|実行時間 [s]<br>4スレッド|
|:--|:--|:--|:--|:--|
|Intel   |`system_clock` |0.133|0.073|0.063|
|        |`cpu_time`     |0.125|0.125|0.125|
|        |`date_and_time`|0.130|0.067|0.033|
|        |`omp_get_wtime`|0.130|0.066|0.033|
|PGI     |`system_clock` |0.110|0.064|0.051|
|        |`cpu_time`     |0.108|0.057|0.029|
|        |`date_and_time`|0.109|0.058|0.029|
|        |`omp_get_wtime`|0.109|0.058|0.029|
|gfortran|`system_clock` |3.145|1.646|1.045|
|        |`cpu_time`     |3.130|3.260|3.859|
|        |`date_and_time`|3.139|1.692|0.959|
|        |`omp_get_wtime`|3.142|1.641|0.937|

- `cpu_time`を用いると，Intelコンパイラとgfortranはスレッド数を増加させても実行時間が減少しません．`cpu_time`はCPU時間を取得するため，CPUの複数コアが動作した場合に各コアの時間の合計が得られているのではないかと考えられます．一方で，PGIコンパイラでは`date_and_time`や`omp_get_wtime`と同様の結果が得られています．~~PGIコンパイラの実装がアレなだけでは･･･~~
- `system_clock`による測定結果はスレッド数の増加とともに減少しますが，2スレッド→4スレッドでの減少は他の手続による結果と比較して少なく，実時間よりも大きな値が得られます．こうなる理由はわかりません．
- `date_and_time`は概ねスレッド数に反比例して実行時間が減少します．
- `omp_get_wtime`は`date_and_time`とほぼ同じ結果が得られています．

並列実行時に実時間で実行時間を取得できるのは`date_and_time()`と`omp_get_wtime()`です．両者のうち，`omp_get_wtime()`の方が，二つの時点での測定結果から実行時間を簡単に計算できます．ただし，`omp_get_wtime()`はモジュールの利用とコンパイルオプションの付与という一手間が必要です．

[^workshare]: コンパイラによる最適化の違いです．gfortranでも，ループを使わずに`c=a+b`と書き直して`!$omp workshare`ディレクティブで並列化すると0.06秒程度まで高速化します．

#まとめ
簡便でかつ並列実行時に正しく時間が測定できるサブルーチン/関数はありません．いずれのサブルーチン/関数にも，正しく計測できない・手間がかかるなどの問題はありますが，本記事の結果より，逐次実行時の実行時間の測定には（入力待ちなどがないのであれば）`cpu_time()`，並列実行時には`omp_get_wtime()`を用いるのがよいと結論づけることができます．

各サブルーチン/関数を用いる際の注意点を示してまとめとします．

## system_clockを利用する場合
- `integer(int64)`を利用する．
- 1秒あたりのカウント数は必ず`system_clock()`の引数から取得する．
- 並列化されたプログラムの実行時間を正しく測定できない．

## cpu_timeを利用する場合
- 得られる時間は実時間ではない．
- 並列化されたプログラムの実行時間を正しく測定できない．

## date_and_timeを利用する場合
- PGIコンパイラは標準時が標準世界時とは異なる．
- 実行時間の計算をユーザが書く場合には，その管理が必要になる．

## omp_get_wtimeを利用する場合
- OpenMPをサポートしていないコンパイラでは利用できない．
- モジュール`omp_lib`を`use`する．
- コンパイル時にOpenMPディレクティブを処理するコンパイルオプションを付与する．

# 補足：連続メモリアクセスする場合
[並列プログラムの実行時間の測定](#並列プログラムの実行時間の測定)で示したコードは，Fortran的にはよいコードではありませんでした[^fool]．Fortranの多次元配列は，最内（最も左側）次元でメモリアドレスが連続になるように割り付けられるので，2重の`do`ループの順序が悪く，各配列に対して連続的にメモリアクセスできません．コメントで指摘をいただいたので，この節ではループの順序を修正して実行時間を測定した結果をまとめます．

[^fool]: かなり初歩的な間違いでした．アイコンのfはfoolのfと言われても反論できません．

`do`ループの順序を以下のように変更した以外，コンパイルオプション等の条件は全く同じです．

```Fortran
    do j = 1,N
    do i = 1,N
        c(i,j) = a(i,j) + b(i,j)
    end do
    end do
```

結果を表にまとめます．

|コンパイラ|時間測定の手続|実行時間 [s]<br>1スレッド|実行時間 [s]<br>2スレッド|実行時間 [s]<br>4スレッド|
|:--|:--|:--|:--|:--|
|Intel   |`system_clock` |0.113|0.069|0.048|
|        |`cpu_time`     |0.109|0.115|0.109|
|        |`date_and_time`|0.114|0.060|0.029|
|        |`omp_get_wtime`|0.112|0.057|0.027|
|PGI     |`system_clock` |0.112|0.063|0.050|
|        |`cpu_time`     |0.111|0.057|0.028|
|        |`date_and_time`|0.112|0.057|0.028|
|        |`omp_get_wtime`|0.111|0.057|0.028|
|gfortran|`system_clock` |0.113|0.063|0.045|
|        |`cpu_time`     |0.109|0.094|0.031|
|        |`date_and_time`|0.113|0.058|0.029|
|        |`omp_get_wtime`|0.113|0.057|0.028|

コンパイラごとの傾向は以下の通りです．
 
- Intelコンパイラは，1割ほど高速化した以外，スレッド数が変化した時のサブルーチン/関数の挙動は同じです．`cpu_time()`は値が変動して，まれに小さい値が得られますが，平均的には同じ挙動とみてよいでしょう．
- PGIコンパイラはわずかに高速化する以外は，全く同じ挙動です．むしろ1スレッド実行時に遅くなっているのは大丈夫なのでしょうか．
- gfortranの実行時間は大幅に短縮され，IntelおよびPGIコンパイラと同等になりました．`cpu_time()`の挙動が変わっており，1スレッド，2スレッド実行時の結果は，全コアのCPU時間が得られていると考えられますが，4スレッド実行時は`date_and_time()`や`omp_get_wtime()`と同程度の結果となりました．