PHP7.4のFFIで遊んでみる

2019-10-15
PHP
FFI
PHP7.4

PHP7.4でFFI(Foreign Function Interface)が使えるようになります。というわけで遊びましょう。

FFIとは?

要するに.dll.soの共有ライブラリをロードして、PHPからCの関数をコールしたり、構造体いじったり出来ます。ネイティブ実行なので処理速度が速いという期待があります。さて、どれくらい速いのか?わざとらしいサンプルで検証してみます。

モンテカルロ法

ランダムな0~1の任意の数値を利用して円周率を計算する方法です。試行回数が多ければ多いほど精度が上がるのですが、実行時間が長くなります。

PHPのソース

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

define('RAND_MAX', 2147483647);

function calcPi(int $n) :float {
$count = 0;
for($i=0; $i<$n ;$i++){
$x = rand(0,RAND_MAX)/RAND_MAX;
$y = rand(0,RAND_MAX)/RAND_MAX;
if($x*$x + $y*$y <= 1){
$count++;
}
}
return 4*$count/$n;
}
echo sprintf("PI => %f\n", calcPi(10000000));

RAND_MAXはC言語側のソースコードと精度を合わせたいので定義してます。

実行時間は

1
2
3
4
5
6
$ time php monte.php
PIE => 3.141581

real 0m5.882s
user 0m5.824s
sys 0m0.061s

C言語のソース

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

double calcPi(int n);

int main() {
printf("PI = %f", calcPi(10000000));
return(0);
}

double calcPi(int n){
int count;
double x,y;
srand(time(NULL));
count = 0;
for(int i = 0; i < n; ++i) {
x = (double)rand() / RAND_MAX;
y = (double)rand() / RAND_MAX;
if( (x * x + y * y) <= 1 ) {
count++;
}
}
return (double) count / n * 4;
}

実行時間は

1
2
3
4
5
6
$ gcc monte.c -o monte
$ time ./monte
PI = 3.141165
real 0m0.377s
user 0m0.372s
sys 0m0.004s

やはり、ネイティブ実行は速い!というわけで、今度はFFIを使ってPHPからCのライブラリを呼び出して見ましょう。
PHPでモンテカルロ法の計算を行うのではなく、計算処理をC言語のネイティブ実行に任せることで処理速度の向上が見込めるはずです。

FFIを使ったモンテカルロ法

まずは、共有ライブラリを作ります。ソースコードは先程のC言語のコードとほぼ変わりません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

double calcPi(int n);

double calcPi(int n){
int count;
double x,y;
srand(time(NULL));
count = 0;
for(int i = 0; i < n; ++i) {
x = (double)rand() / RAND_MAX;
y = (double)rand() / RAND_MAX;
if( (x * x + y * y) <= 1 ) {
count++;
}
}
return (double) count / n * 4;
}

それっぽい雰囲気を出すために、libmonte.cという名前で保存します。

共有ライブラリとしてコンパイルして、libmonte.soを作成。

1
$ gcc -fPIC -shared -o libmonte.so libmonte.c

FFIを使って、libmonte.socalcPi関数を呼び出します。かなりシンプルに書けますね。

1
2
3
<?php
$ffi = FFI::cdef( "double calcPi(int n);", __DIR__."/libmonte.so");
echo sprintf("PI => %f\n", $ffi->calcPi(10000000));

実行時間は…

1
2
3
4
5
6
$ time php ffi.php
PI => 3.142153

real 0m0.466s
user 0m0.413s
sys 0m0.058s

いやー、わざとらしく速くなりました。めでたしめでたし。FFI初めて使ってみましたが、書き方もシンプルだし良いですね!

まとめ

というわけで、FFIを使うことで時間のかかる計算処理が大分速く実行できるというサンプルでした。この他にも色んな使いみちがありそうです。

次は、FFIがPHPのソースコード内でどのような動作をしているのかを追ってみようと思います。