NVIDIA HPC SDK nvfortranの挙動を他のコンパイラと比較する

# 概要
これまでに，Fortranの基本的な機能を色々と調べてきたので，PGI Fortranからnvfortranに代わり，挙動がどう変化したのかを調査しました．

重要なのは，下記の点くらいです．

- 基本的な挙動はPGI Fortranと同じ
- 4倍精度実数は相変わらずサポートされていない
- 配列構成子内での明示的な型変換，allocatableな配列への一括代入時の挙動が，どのコンパイラとも異なる
- 派生型の機能は，他のコンパイラに少々及ばない

これまでPGI Fortranを使う上で気をつけていたことをそのまま気をつけていれば[^eg]，特に大きな問題は生じないと思われます．

[^eg]: つまり，あまり凝ったことはしない方が無難だということです．

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

# 整数・実数・複素数型関係
ほとんどの挙動は，[Fortranにおける整数型・実数型・複素数型変数の宣言方法](https://qiita.com/implicit_none/items/ab0632357bab0f549cf2)のPGI Fortranと同じです．

また，下記についても，PGI Fortranと同じ挙動でした．

- [Fortranにおける浮動小数点の丸めと整数化のモード](https://qiita.com/implicit_none/items/63d1e84ed9a84f5abc9e)
- [Fortranの逆三角関数を使って円周率を求める](https://qiita.com/implicit_none/items/1a345626afe8b1fa750a)
- [FortranでNaNを定数として宣言する何通りかの方法](https://qiita.com/implicit_none/items/a96d5bdb1a0cf05b6f03)

Intel Fortranやgfortranとの違いは，下記小節にまとめています．

## 2進数，8進数，16進数リテラルの表示の差異
Fortranで2,8,16進数のリテラルを扱うには，基数を表す記号の後ろに`' '`あるいは`" "`で囲んだ数値を配置します．
基数を表す記号は，2進数は`B`，8進数は`O`，16進数では`Z`で，まとめて`BOZ`とよばれています．

```Fortran
print *,B'11',O'77',Z'FF'
```

2,8,16進数のリテラルを画面表示する際，Intel Fortranおよびgfortranは，10進数に変換して表示します．

```console
 3 63 255
```

一方で，nvfortranは16進数で表示されます．

```console
00000003 0000003F 000000FF
```

10進数で表示するには，明示的に整数型に型変換する必要があります．

```Fortran
print *,int(B'11'),int(O'77'),int(Z'FF')
```

```console
            3           63          255
```

## 4倍精度実数の未サポート
PGI Fortranは4倍精度実数をサポートしていませんでしたが，nvfortranも同様です．
`selected_real_kind`で倍精度実数の範囲を超える桁数や指数の範囲を指定すると，桁数や指数の要件を満たすことができない場合に返されるステータス`-3`が返ってきます．

```Fortran
print *,selected_real_kind(p=33,r=4931)
```

```console
 -3
```

4倍精度実数の指数記号である`q`を使うとコンパイルエラーになりますし，`iso_fortran_env`内では，`real128`は`-1`として定義されています．

# 配列関係
ほとんどの挙動は，[Fortranにおける配列の宣言方法と関連機能](https://qiita.com/implicit_none/items/08f894bc851c29b40b58)のPGI Fortranと同じですが，PGI FortranやIntel Fortran, gfortranとも異なる挙動をする処理もあります．

## 配列構成子内での明示的な型変換がエラーになる
配列の要素に異なる値を一括で代入したい場合には，配列構成子`[]`が利用できます．`[`と`]`の間に数値リテラルや変数（配列を含む）を記述することで，配列リテラルを構成します．

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

    real(real32) :: a(5)
    real(real32) :: b(5)
    real(real32) :: c(5)

    a(:) = [0.1, 0.2, 0.3, 0.4, 0.5]
    b(:) = [1, 2, 3, 4, 5]                   !暗黙の型変換
    c(:) = [real(real32) :: b(1), a(2:4), 0] !明示的な型変換
end program main
```

配列構成子内で数値リテラルのみを用いる場合，暗黙の型変換によって適切な型に変換されます．
一方で，配列構成子に変数を用いる場合，変数の型が混在していると，Intel Fortranとgfortranではコンパイルエラーになります（PGI fortranおよびnvfortranではエラーになりませんでした）．そのような場合には，配列構成子`[]`内の一番左に`型名 ::`を置いて，明示的に型変換を行う必要があるのですが，nvfotranでは明示的な型変換を行うとエラーが出ます．

PGI Fortranでは明示的な型変換はしてもしなくても問題なかったのですが，nvfortranでは明示的な型変換がエラーになるので，コンパイラ間の移行作業の際は，気を付ける必要がありそうです．

## 配列定数の暗黙形状宣言には（未だ）未対応
配列の定数を宣言すると，配列の要素数や形状はあらかじめ決まります．Fortran 2008では，この性質を利用して，配列の定数を宣言する際にその要素数の記述を簡略化できます．暗黙の形状配列宣言は，配列要素番号の下限と上限を指定する形式に似ていますが，上限を*として，上限は右辺から決められるようにします．

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

    integer,parameter :: a(1:*) = [1,2,3,4]
    integer,parameter :: b(1:*,-1:*) = reshape([1,2,3,4,5,6],[3,2])

    print *,size(a),shape(a) ! 4 4
    print *,a(:)             ! 1 2 3 4

    print *,size(b),shape(b)    ! 6  3  2
    print *,lbound(b),ubound(b) ! 1 -1  3  0
    print *,b(:,:)              ! 1  2  3  4  5  6
end program main
```

（PGI Fortranと同じく）nvfortranは，この配列の暗黙形状宣言には対応していません．

## 異なる形状の配列同士の一括代入時の挙動
配列の要素番号の代わりに`(:)`を用いると，配列の全要素に対する処理であることを明記できます．

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

    real(real32) :: a(5)
    real(real32) :: b(5)
    real(real32) :: c(5)

    a(:) = 1.0 !a(1)~a(5)に1.0を代入
    b(:) = 2.0 !b(1)~b(5)に2.0を代入
    c(:) = 0.0 !c(1)~c(5)に0.0を代入

    c(:) = a(:) + b(:) !配列の各要素同士の和を計算
    print *,c(:)
    !   3.000000       3.000000       3.000000       3.000000       3.000000
end program main
````

配列形状を静的に（宣言時に）決定した場合は，`(:)`を使って異なるサイズの配列同士の演算を行うことはできませんが，`allocatable`な配列の場合は，`(:)`を使って異なるサイズの配列同士の演算を行うことができます．

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

    integer(int32),allocatable :: a(:)
    integer(int32) :: b(5)
    integer(int32) :: c(3)

    allocate(a(5),source=1)
    b(:) = 2
    c(:) = 3

    print *,a(:)+c(:) ! 3 3 3 1 1
    print *,b(:)+c(:) ! <- コンパイルエラー
end program main
```

2次元配列に対して，`(:)`を使って異なるサイズの配列同士の演算を行うと，PGI Fortranとnvfotranは挙動が怪しくなります．

下の例では，[3,2]の配列と[2,2]の配列の足し算をしています．

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

    integer(int32),allocatable :: b(:,:)
    integer(int32) :: c(2,2) = reshape([4,3,2,1], [2,2])

    allocate(b,source=reshape([1,2,3,4,5,6], [3,2]))
    print *,b(:,1)
    print *,b(:,2)
    print *

    b(:,:) = c(:,:)
    print *,b(:,1)
    print *,b(:,2)
    deallocate(b)
end program main
```

Intel fortranとgfortranは，`b(:,:) = c(:,:)`が`b(1:2,1:2) = c(1:2,1:2)`と展開されて代入が行われています．

```console
 1 2 3
 4 5 6

 4 3 3
 2 1 6
```

PGI Fortranの挙動は，よくわかりません．

```console
 1 2 3
 4 5 6

 4 3 2
 2 1 1
```

nvfortranの挙動もよくわかりません．

```console
 1 2 3
 4 5 6

 4 3 2
 2 1 0
```

Fortran規格でどう定義されているかを調べなければなんとも言えませんが，コンパイラを移行した際に，思わぬ挙動に遭遇する可能性があるので，こうなることは知っておいて損はないでしょう．

# 文字型変数，文字列関係
挙動は，[Fortranにおける文字型変数の宣言方法と関連機能](https://qiita.com/implicit_none/items/f582f2d4e10f2655e208)と同じでした．また，整数の文字列への変換（[Fortranで整数を文字列に変換する](https://qiita.com/implicit_none/items/39018f2848b5237fc3ea)）も，問題なく動作しました．

文字列に関して，特に問題のある挙動は見られません．コンパイラ間の可搬性という意味では，下記2点に気を付ける事になります．

- エスケープ文字の取り扱い
    - Intel Fortran, gfortranはバックスラッシュを通常の文字として扱うが，PGI Fortranおよびnvfortranは，エスケープ文字として扱う．
    - バックスラッシュを通常の文字として扱うには，コンパイルオプション`-Mnobackslash`を付与する必要があるので，標準のコンパイルオプションとして付ける癖を付けた方がよい．
- 文字種別が4の文字の取り扱い
    - PGI Fortranおよびnvfortranは，Unicode向けの利用を想定した文字種別（`kind=4`）が利用できない．
    - 文字種別を確認するための定数`character_kinds`が定義されていない．

# ユーザ定義派生型関係

ほとんどの挙動は，[Fortranにおける派生型の基本的な使い方](https://qiita.com/implicit_none/items/12c5404a11e6c237d100)と同じです．

## 再帰型の未サポート
いわゆる構造体に相当するユーザ定義派生型は，自身の型の変数を成分に持つことはできません．ただし，`pointer`属性および`allocatable`属性を付与した派生型変数であれば，同一派生型の成分として持つことができます．

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

    type :: vector2d
        real(real32) :: x
        real(real32) :: y
        type(vector2d),pointer     :: ptr ! ポインタ変数であれば成分として宣言できる．
        type(vector2d),allocatable :: vec ! allocatable属性があれば成分として宣言できる．
    end type vector2d
end program main
```

このように，`allocatable`属性を付与した自身の型の変数を成分としてもつ派生型を，再帰型（recursive type）とよびます．

PGI Fortranと同様に，nvfortranはこの再帰型には対応していません．`pointer`属性は，昔からメモリリークなどの問題の原因となっていたので，自動で解放される`allocatable`属性を利用できる方が，安全性という意味で非常に好ましいと考えられます．早めに対応してもらいたいところです．

## ユーザ定義派生型IO
ユーザ定義派生型IOについては，[Fortranのユーザ定義派生型の出力用サブルーチンの書き方](https://qiita.com/implicit_none/items/ad436b389439f8bed4bb)や[Fortranで区間演算](https://qiita.com/implicit_none/items/3eb55d92688491570ac2)のプログラムがコンパイルできません．

その理由は，モジュール内を`private`とし，モジュール外部へ変数やサブルーチンを公開するための記述`public write (formatted)`に対応していないためです．

対策は2通りあります．[Fortranのユーザ定義派生型の出力用サブルーチンの書き方](https://qiita.com/implicit_none/items/ad436b389439f8bed4bb)の`tuple2`型を例に説明すると，

1. モジュール内全てを`public`とする．

その場合も，`interface`には修正が必要です．

```Fortran
    interface write(formatted)
        ! procedure printTuple2 !Intel Fortran およびgfortran
        module procedure printTuple2 ! nvfortran
    end interfac
```

2. ユーザ定義派生型IO用のサブルーチンを，型束縛手続とする．

型束縛手続として型に含め，`generic`でIO用サブルーチンとして関連付けます．モジュール内部を`public`にしたくない場合には，これが最善の対策といえるでしょう．

```Fortran
    type,public :: tuple2
        real(8),public :: x
        real(8),public :: y

        contains

        procedure, public, pass :: printTuple2
        generic :: write(formatted)=>printTuple2
    end type tuple2
```

# FortranとCの相互運用関係
FortranとC言語の相互運用について，以前に掲載した記事ではPGI Fortranを取り扱っていませんでしたが，nvfortranとnvc++を使って実行したところ，挙動は同じでした．
ただし，`sprintf_s`は利用できないので，`sprintf`に書き換えています．

- [Fortranの派生型とC言語の構造体の相互運用](https://qiita.com/implicit_none/items/86e0a16a1131c66437ad)
- [Fortranでポインタ変数のアドレスをインクリメントする](https://qiita.com/implicit_none/items/1c33fc6c8daf50da28f0)

# 時間測定関係

[Fortranにおける実行時間の測定](https://qiita.com/implicit_none/items/dd067e9bf5f7b49ce84b)と比較して，サブルーチン`date_and_time`の挙動に1点修正がありました．また，時間測定に関して，（変わっていないからこそ）気をつけなければならない挙動について記しておきます．

## `date_and_time`のタイムゾーン
サブルーチン`date_and_time`では，サブルーチン呼出し時点の年月日と時刻（時，分，秒，ミリ秒）が得られます．このとき，PGI Fortranでは，標準時からの時差が0になっており，OSのタイムゾーンを標準時としているような挙動でした．

```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
 20200613
 133503.719
 +0000 ! 標準時からの時差
         2020            6           13            0           13           35
            3          719
```

nvfortranでは，Intel Fortranやgfortranと同じく，協定世界時を標準時とするように変わっています[^time]．

[^time]: PGI Fortranのどこかのバージョンで変わっていたのかも知れませんが，そこまでは調べられていません．

```console
 20200613
 133517.124
 +0900 ! 標準時からの時差
         2020            6           13          540           13           35
           17          124
```

## `cpu_time`の挙動
`cpu_time`の挙動は，PGI Fortranと同じく，Intel Fortran, gfortranのそれとは異なり，注意が必要です．

```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
```

`cpu_time`で`sleep()`前後の時間を測定すると，Intel Fortran, gfortranでは前後の時間が等しくなりますが，nvfortranでは前後の時間が等しくなりません．

`cpu_time()`は，その名前の通りCPU時間を取得するサブルーチンです．`sleep()`はCPUを（正確にはプロセス）を停止するので，Intel Fortran, gfortranでは`sleep()`前後の時間が等しくなりました．nvfortranは，どうも実時間が得られているようです．

また，処理を並列化した場合，Intel Fortran, gfortranでは`cpu_time()`によって全CPU時間の合計が得られるので，測定された時間は使用するコア数によって変化しません．一方で，nvfortranは，処理を並列化して実行時間が短縮されると，測定された実行時間も短くなります．

コンパイラに依存せず，実時間を測定したいという要望に対しては，`date_and_time()`か`omp_get_wtime()`の使用が推奨されます．だたし，`date_and_time()`を用いる場合は，年月日と時刻の情報から実行時間を計算する処理を記述する必要があります．

# まとめ
現在(バージョン20.5)の時点では，nvfortranは名前こそ変わっていますが，PGI Fortranと同じものと見なせるでしょう．
NVIDIA HPC SDKと名前を変えたことで何かが変わったということはなさそうです．ただし，コンパイルオプションには，`cc80`が既に追加されていて，対応が早くなったように感じます．

GTC 2020の講演では，HPC GPUプログラミングモデルはCUDA Fortran，OpenACCからISO FORTRANになってProgrammabilityが大幅に向上するようなことが言われていたので，今後はFortran 2008の機能も随時追加されていくと期待しています．