🎃
pybind11を使ってC++/FortranプログラムをPythonで動かす <その2>
はじめに
Python ➔ C++ ➔ Fortranの順にバインディングを行うことで、PythonからFortranプログラムを動かすテストの第2回です。今回は引数を介して言語間でデータの受け渡しをしてみます。入力パラメータがPython ➔ C++ ➔ Fortranの順に流れ、計算結果がFortran ➔ C++ ➔ Pythonの順に返ってくるイメージです。
プログラム例 2
2つの整数(または浮動小数点数)の和(または差)を計算するFortranプログラムを作成します。
arithmetic.f90
module add_mod
use, intrinsic :: iso_c_binding, only: c_int
implicit none
contains
! Add two integers
function add_int(i, j) bind(C)
integer(c_int), intent(in), value :: i, j
integer(c_int) :: add_int
add_int = i + j
end function
end module
module subtract_mod
use, intrinsic :: iso_c_binding, only: c_double
implicit none
contains
! Subtract two floating-point numbers
function subtract_double(a, b) bind(C)
real(c_double), intent(in) :: a, b
real(c_double) :: subtract_double
subtract_double = a - b
end function
end module
これを呼ぶC++プログラムです。
example.cpp
#include <pybind11/pybind11.h>
// Using Fortran code within C++
extern "C" {
int add_int(const int i, const int j);
double subtract_double(const double *a, const double *b);
}
// Add two integers
int c_add(const int i, const int j) {
return add_int(i, j);
}
// Substract two floating-point numbers
double c_subtract(const double a, const double b) {
return subtract_double(&a, &b);
}
// Bindings with Python
PYBIND11_MODULE(example, m) {
m.def("add_int", &c_add);
m.def("subtract_double", &c_subtract);
}
ポイントは、
- 整数はFortranのvalue属性で値渡し
- 浮動小数点数はC++のポインタで参照渡し
としていることです。
Pythonから動かしてテストしてみます。
>>> import example
>>> x = 5
>>> y = 7
>>> example.add_int(x, y)
12
>>> example.subtract_double(x, y)
-2.0
プログラム例 3
次はPythonのリスト(配列)を渡してみます。C++の側はvector型で受け取ることにして、そのポインタをFortranに渡して計算を進めます。
一例として、データ列の平均と分散の計算をしてみます。まずはFortranのプログラム。
statistic.f90
module statistics_mod
use, intrinsic :: iso_c_binding, only: c_int, c_double
use, intrinsic :: iso_fortran_env, only: real64
implicit none
contains
! Calculate mean and variance
subroutine statistics(n, x, mean, var) bind(C)
integer(c_int), intent(in), value :: n
real(c_double), intent(in) :: x(n)
real(c_double), intent(inout) :: mean, var
real(real64) :: diff(n)
mean = sum(x) / n
diff(1:n) = x(1:n) - mean
var = dot_product(diff, diff) / (n - 1)
end subroutine
end module
これまでと同様にして、C++プログラムも作成します。計算結果はタプルにして返り値で返すようにしています。これはPythonの変数がイミュータブルで、引数を使ってデータを戻せないからです。
example.cpp
#include <vector>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
// Using Fortran code within C++
extern "C" {
void statistics(const int n, const double *x, double *mean, double *var);
}
// Mean and variance
std::tuple<double, double> c_statistics(const std::vector<double> &v) {
const int n = v.size();
const double *v_ptr = v.data();
double mean, var;
statistics(n, v_ptr, &mean, &var);
return std::forward_as_tuple(mean, var); // C++11
// return {mean, var}; // C++17
}
// Bindings with Python
PYBIND11_MODULE(example, m) {
m.def("statistics", &c_statistics);
}
ここでのポイントは、
- STLコンテナを使ってPythonのリストからC++のvectorへ自動変換を行う点
にあります。そのため、pybind11/stl.h ヘッダのインクルードが必要です。
Pythonから動かしてテストしてみます。検証目的でPython単独(statisticsモジュール)の計算もしてみました。
>>> import example
>>> import numpy as np
>>> rng = np.random.default_rng()
>>> x = rng.normal(5, 10, 1000)
>>> print('Fortran: (mean, var) =', example.statistics(x))
Fortran: (mean, var) = (4.449369335303001, 106.21722645850859)
>>> import statistics
>>> print('Python: (mean, var) =', statistics.mean(x), statistics.variance(x))
Python: (mean, var) = 4.449369335303006 106.21722645850852
メリット・デメリット
今回の例は、PythonとC++のバインディングにおいて、
- C++側を標準仕様のstd::vectorのまま書き換えずに運用できる
という点でメリットがある一方で、
- Pythonのリスト型データからC++のvector型データへの自動変換がコピーセマンティクスにより行われる
というデメリットも孕んでいます。そこで次回はコピーセマンティクスに拠らない手法をテストします。
参考
Discussion