【実務・中級編】C言語連携における配列の形状(Shape)情報の伝達 – モダンFortran言語仕様と実践実践マスター

FortranとCの境界線:配列形状(Shape)の「危うい相互運用」を制する

数値計算の現場において、Fortranの計算カーネルをC/C++のホストプログラムから呼び出す、あるいはその逆を行う機会は避けられない。しかし、多くのエンジニアがここで「セグメンテーションフォールト」という名の深淵に足を踏み入れる。

Fortranの多次元配列は「列優先(Column-major)」であり、Cのそれは「行優先(Row-major)」だ。この根本的なメモリレイアウトの不一致を、単なる「型変換」の問題として片付けてはならない。今回は、コンパイラの最適化を阻害せず、かつメモリの整合性を保証する「堅牢な境界設計」を伝授する。

1. 「形状伝達」の黄金律:Descriptorを隠蔽せよ

Fortranの配列(特にアロケータブルやポインタ)をC側に渡す際、最もやってはいけないのが「コンパイラ固有の配列記述子(Descriptor)をC側で無理やり構造体として再現すること」だ。コンパイラやバージョンによってメモリレイアウトが異なるため、移植性が皆無になる。

正解は、「生ポインタ」と「次元ごとのサイズ情報」を個別に渡すことだ。

! — Fortranサイド: C公開用ラッパー —
subroutine compute_kernel_c(data, n1, n2) bind(c, name=”compute_kernel”)
use, intrinsic :: iso_c_binding
implicit none

! C側からはポインタとして受け取る
type(c_ptr), value :: data
integer(c_int), value :: n1, n2

! ローカルでFortranの配列ポインタにマッピングする
real(c_double), pointer :: f_data(:, 🙂

! c_f_pointerを使うことで、Cの生メモリをFortranの配列として扱う
! ここで列優先順序が正しく適用されるように形状を定義する
call c_f_pointer(data, f_data, [n1, n2])

! あとはFortranの爆速な最適化エンジンに任せる
call perform_heavy_calc(f_data)
end subroutine

2. メモリレイアウトの「泥臭い」注意点

Fortranは `A(i, j)` とアクセスするとき、メモリ上で隣接するのは `A(i+1, j)` である。ループを回す際、最も内側の添字を左側に置くのが鉄則だ。

もしC側から渡されたメモリがRow-major(`[j][i]`)で確保されている場合、Fortran側で `f_data(i, j)` とアクセスすると、計算のたびにストライドの大きいアクセスが発生し、キャッシュミスが多発する。

  • 鉄則: Fortranのカーネルを主軸にするなら、C側で確保するメモリも「Fortranの論理的な転置」を考慮したサイズで確保せよ。つまり、`[N_col][N_row]` ではなく `[N_row][N_col]` の順でメモリを確保し、Fortran側へ渡すことが、ベクトル化を最大限に活かす鍵だ。

3. 最適化を殺さないためのインターフェース設計

コンパイラは `bind(c)` を見ると、外部からの副作用を警戒して最適化を緩めることがある。これを防ぐには、`intent` 属性を明示し、`contiguous` 属性を付与して「配列がメモリ上で隙間なく並んでいる」ことをコンパイラに保証することだ。

subroutine perform_heavy_calc(arr)
! contiguousを付けることで、コンパイラはポインタの指す先が
! 連続領域であることを確信し、SIMD命令(AVX-512等)を積極的に展開する
real(c_double), intent(inout), contiguous :: arr(:, 🙂

integer :: i, j, n1, n2
n1 = size(arr, 1)
n2 = size(arr, 2)

! ループ展開とベクトル化の恩恵を最大化する書き方
! 内側のループが第一添字であることに注意
do j = 1, n2
do i = 1, n1
arr(i, j) = arr(i, j) 2.0d0
end do
end do
end subroutine

4. 実務でハマる「ビルド時の罠」

最後に、コンパイルフラグについて言及する。Intel Fortran (ifort/ifx) や GNU Fortran (gfortran) を使う際、以下の設定を怠ると、デバッグビルドで動いたものがリリースビルドで壊れるという悪夢を見る。

  • `fpe-all=0`: 浮動小数点例外をトラップする。境界チェック (`-check bounds`) は開発時には必須だが、リリース時には必ず外すこと。
  • `-qopt-report` (Intel): これを使え。コンパイラが「なぜベクトル化できなかったか」を詳細なレポートとして出力してくれる。これを見ずに「Fortranは速い」と信じ込むのは、宗教に近い。

まとめ:現場の知見

1. Cとの境界は `iso_c_binding` 一択。 独自実装の構造体は負債でしかない。
2. `c_f_pointer` でポインタをFortran配列にキャストする。 このとき、`contiguous` を忘れると最適化レベルが一段階落ちる。
3. アクセス順序は列優先を遵守。 C側で確保するメモリの定義をFortran側に合わせるのが最も低コストな最適化だ。

大規模なシミュレーションコードにおいて、データ転送のオーバーヘッドは誤差かもしれないが、メモリキャッシュの効率は死活問題だ。Fortranが持つ強力な配列演算のポテンシャルを、Cという「窓口」で殺さないこと。それがシニアアーキテクトとしての我々の務めである。

コメント

タイトルとURLをコピーしました