TL;DR
- Zephirを使ってPHPの拡張モジュールを作成・テストする方法について書いています
- 任意の整数$n$に対してフィボナッチ数列$F_{n}$を求めるプログラム(再帰版)を使ってPHPとのパフォーマンスを比較したところ、$F_{38}$ではZephirの方がPHPより約19倍高速になりました
動作確認環境
この記事の動作確認環境は次の通りです。
- OS
- macOS High Sierra 10.13.6
- PHP
- 7.2.10(phpenvでインストール)
- Zephir
- 0.11.8
- Zephir Parser
- 1.1.3
コード
この記事で使ったコードはGitHubに公開しています。
Zephirとは?
Zephir(Zend Engine PHP Intermediate)とは、PHPの拡張モジュールの作成を容易にすることを目的として開発されたプログラミング言語です。
Zephirで作成したプログラムはC言語のコードに変換され、C言語のコンパイラによってコンパイルされます。そのためPHPより高速に動作します。
また、C言語と違いポインタや直接的なメモリ操作が許されていないので、直接C言語で書くより簡単に拡張モジュールを書くことができます。
Zephirは動的型と静的型の両方を持ちます。
変数に型宣言を書くことで、PHPより型安全にプログラムを書くことができます。
文法はPHPに似ており、PHPを書いたことがある人であればすぐに馴染めると思います。
PHPの組み込み関数を使用することもできます。
インストール
Composerを使ってインストールできます1。
$ composer global require phalcon/zephir
zephir
コマンドを実行して「zephir」と表示されればインストールできています。
$ zephir
_____ __ _
/__ / ___ ____ / /_ (_)____
/ / / _ \/ __ \/ __ \/ / ___/
/ /__/ __/ /_/ / / / / / /
/____/\___/ .___/_/ /_/_/_/
/_/
Zephir 0.11.8 by Andres Gutierrez and Serghei Iakovlev (source)
Usage:
command [options] [arguments]
Options:
-h, --help Display this help message
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi Force ANSI output
--no-ansi Disable ANSI output
-n, --no-interaction Do not ask any interactive question
--dumpversion Print the Zephir version — and don't do anything else
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
api Generates a HTML API based on the classes exposed in the extension
build Generates/Compiles/Installs a Zephir extension
clean Cleans any object files created by the extension
compile Compile a Zephir extension
fullclean Cleans any object files created by the extension (including files generated by phpize)
generate Generates C code from the Zephir code without compiling it
help Displays help for a command
init Initializes a Zephir extension
install Installs the extension in the extension directory (may require root password)
list Lists commands
stubs Generates stubs that can be used in a PHP IDE
zephir build
コマンドの実行時にないとエラーになるので、Zephir Parserもインストールしておきます。
$ git clone git://github.com/phalcon/php-zephir-parser.git
$ cd php-zephir-parser
$ phpize
$ ./configure
$ make
$ sudo make install
インストール後、php.ini
に拡張モジュールを追加します。
[Zephir Parser]
extension=zephir_parser.so
拡張モジュールの作成
zephir init
コマンドを実行すると、Zephirのプロジェクトが作成されます。
今回はmyzephir
という名前空間でプロジェクトを作成します。
$ zephir init myzephir
コマンドを実行すると、カレントディレクトリにmyzephir
というディレクトリが作成されます。
これがプロジェクトディレクトリです。
また.zephir
というディレクトリも作成されます。
これはキャッシュディレクトリなので、~/.gitignore
に追加してgit管理から無視するように設定しておいた方がいいと思います。
myzephir
ディレクトリは次のような構成になっています。
ext/
kernel/
myzephir/
ext
ディレクトリには、C言語のコンパイラによって使用されるファイルが配置されます。
これらのファイルはzephir build
コマンドを実行するたびに生成されるので、次のようにext
ディレクトリの中に.gitignore
ファイルを作成して、ext
ディレクトリの中のすべてのファイルをgit管理から無視するように設定してしまって大丈夫だと思います。
*
!.gitignore
myzephir
ディレクトリには、Zephirで書いたプログラムを配置していきます。
今回は例として、任意の整数$n$に対してフィボナッチ数列$F_{n}$を求めるプログラム(再帰版)の拡張モジュールを作成してみます。
myzephir
ディレクトリの下にfibonacci.zep
という名前でZephirのプログラムを作成します。
namespace MyZephir;
class Fibonacci
{
public static function fib(int n) -> int
{
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return self::fib(n - 2) + self::fib(n - 1);
}
}
「変数の先頭に$
を付けない」「メソッドの戻り値のタイプヒント指定に->
を使う」といった違いはありますが、雰囲気はPHPに似ていると思います。
なお、Zephirでは名前空間の指定は必須です。
名前空間名はディレクトリ名と対応させる必要があります。PSR-4のような規約です。
今回で言うと、次のようなディレクトリと名前空間になっています。
myzephir/
fibonacci.zep # MyZephir\Fibonacci
プロジェクトルートでzephir build
コマンドを実行すると、拡張モジュールを生成・コンパイル・インストールすることができます。
再度zephir build
コマンドを実行する前にはzephir fullclean
を実行する必要があります。
面倒なので、プロジェクトルートに次のようなMakefileを作成しておくと便利だと思います。
build:
zephir fullclean
zephir build
make build
コマンドの実行前に、config.json
のinternal-call-transformation
をtrue
に変更しておきます。
"internal-call-transformation": true
false
のままだと、全てのメソッド呼び出しがPHPスクリプトを介して行われてしまうので、今回のフィボナッチ数列を求めるプログラムのように再帰呼び出しが非常に多い場合、パフォーマンスが出ません。
試しにfalse
のままmake build
コマンドを実行して拡張モジュールを生成してみたところ、PHPと同程度かやや遅いくらいのパフォーマンスしか出ませんでした。
true
にすると、内部的なメソッド呼び出しがPHPスクリプトを介さずに行われるようになるので、その分高速になります2。
make build
コマンドを実行します。
$ make build
生成された拡張モジュールは拡張モジュールのディレクトリ(extension_dir
に指定されたディレクトリ)に配置されるので、php.ini
に追加するだけでPHPから使用できるようになります。
[My Zephir]
extension=myzephir.so
次のようにして、PHPから作成した拡張モジュールのメソッドを呼び出すことができます。
MyZephir\Fibonacci::fib(10); # 55
ユニットテスト
ユニットテストは通常のPHPプログラムと同じように、PHPUnitからテストしたい拡張モジュールのメソッドを呼び出すことで行えます。
<?php
declare(strict_types=1);
namespace MyZephir;
use PHPUnit\Framework\TestCase as PhpUnitTestCase;
use MyZephir\Fibonacci;
class FibonacciTest extends PhpUnitTestCase
{
public function provide_Fib_CalculationResultIsReturned(): array
{
return [
['n' => 0, 'expectedResult' => 0],
['n' => 1, 'expectedResult' => 1],
['n' => 2, 'expectedResult' => 1],
['n' => 10, 'expectedResult' => 55],
];
}
/**
* @dataProvider provide_Fib_CalculationResultIsReturned
*/
public function test_Fib_CalculationResultIsReturned(int $n, int $expectedResult): void
{
$actualResult = Fibonacci::fib($n);
$this->assertEquals($expectedResult, $actualResult);
}
}
ただし、拡張モジュール内のカバレッジを計測することはできません。
Zephir vs. PHPのパフォーマンスベンチマーク
フィボナッチ数列$F_{0}$〜$F_{38}$を求めるベンチマークプログラムを使って、Zephirのパフォーマンスを計測してみます。
<?php
declare(strict_types=1);
for ($i = 0; $i < 39; $i++) {
$timeStart = microtime(true);
$result = MyZephir\Fibonacci::fib($i);
$time = microtime(true) - $timeStart;
echo sprintf('%.8f', $time) . PHP_EOL;
}
同様にPHPのパフォーマンスを計測し、Zephirと比較すると、下記の結果になりました。
$F_{n}$ | Zephir | PHP |
---|---|---|
$F_{0}$ | 0.00005794 | 0.00014806 |
$F_{1}$ | 0.00000286 | 0.00000286 |
$F_{2}$ | 0.00000501 | 0.00001097 |
$F_{3}$ | 0.00000191 | 0.00001001 |
$F_{4}$ | 0.00000215 | 0.00001502 |
$F_{5}$ | 0.00000215 | 0.00002384 |
$F_{6}$ | 0.00000191 | 0.00005198 |
$F_{7}$ | 0.00000286 | 0.00010681 |
$F_{8}$ | 0.00000310 | 0.00015903 |
$F_{9}$ | 0.00000405 | 0.00027394 |
$F_{10}$ | 0.00000501 | 0.00043893 |
$F_{11}$ | 0.00000787 | 0.00080895 |
$F_{12}$ | 0.00001192 | 0.00100899 |
$F_{13}$ | 0.00004411 | 0.00134993 |
$F_{14}$ | 0.00003314 | 0.00313401 |
$F_{15}$ | 0.00004292 | 0.00422812 |
$F_{16}$ | 0.00009108 | 0.00409389 |
$F_{17}$ | 0.00025702 | 0.00588703 |
$F_{18}$ | 0.00019717 | 0.01006794 |
$F_{19}$ | 0.00031590 | 0.01560998 |
$F_{20}$ | 0.00047088 | 0.03654289 |
$F_{21}$ | 0.00073099 | 0.05400610 |
$F_{22}$ | 0.00120211 | 0.07315898 |
$F_{23}$ | 0.00191689 | 0.11268210 |
$F_{24}$ | 0.00426698 | 0.20995712 |
$F_{25}$ | 0.00957918 | 0.31403303 |
$F_{26}$ | 0.01057291 | 0.53001404 |
$F_{27}$ | 0.01478887 | 0.91901708 |
$F_{28}$ | 0.03116798 | 1.22161698 |
$F_{29}$ | 0.05580115 | 2.02588201 |
$F_{30}$ | 0.07225895 | 3.07808805 |
$F_{31}$ | 0.12849402 | 4.61468101 |
$F_{32}$ | 0.20078897 | 7.38472295 |
$F_{33}$ | 0.34922314 | 12.09803295 |
$F_{34}$ | 0.60511899 | 19.12313700 |
$F_{35}$ | 1.06989408 | 31.32050490 |
$F_{36}$ | 1.99410796 | 49.86270094 |
$F_{37}$ | 3.65922403 | 81.90827513 |
$F_{38}$ | 6.97081399 | 133.79607511 |
Zephirの方が圧倒的に高速ですね。
$F_{38}$では約19倍高速です。
まとめ
Zephirを使うと拡張モジュールを簡単に作成することができます。
PHPでは時間がかかってしまう処理をZephirで拡張モジュール化することで、処理の高速化を実現できそうです。
ただし、ビルドやデプロイなどの考慮が必要になること、テストのカバレッジが計測できなくなることといったデメリットもあります。
メリットとデメリットのトレードオフを見極めた上で使用するかどうかを決めた方がいいと思います。
-
公式ドキュメントにはGitHubからcloneしてくる方法が記載されていますが、これは古い情報のようです。cloneしてきたリポジトリにZephirインストーラ(
install
ファイル)が見当たりませんでした。 ↩ -
この辺りの話はこのpull requestを読むとわかりやすいです。 https://github.com/phalcon/zephir/pull/962 ↩