ZephirでPHPの拡張モジュールを作成する


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に公開しています。

https://github.com/ngmy/zephir-vs-php-performance


Zephirとは?

ZephirZend 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管理から無視するように設定してしまって大丈夫だと思います。


ext/.gitignore

*

!.gitignore

myzephirディレクトリには、Zephirで書いたプログラムを配置していきます。

今回は例として、任意の整数$n$に対してフィボナッチ数列$F_{n}$を求めるプログラム(再帰版)の拡張モジュールを作成してみます。

myzephirディレクトリの下にfibonacci.zepという名前でZephirのプログラムを作成します。


myzephir/fibonacci.zep

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を作成しておくと便利だと思います。


Makefile

build:

zephir fullclean
zephir build

make buildコマンドの実行前に、config.jsoninternal-call-transformationtrueに変更しておきます。


config.json

"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で拡張モジュール化することで、処理の高速化を実現できそうです。

ただし、ビルドやデプロイなどの考慮が必要になること、テストのカバレッジが計測できなくなることといったデメリットもあります。

メリットとデメリットのトレードオフを見極めた上で使用するかどうかを決めた方がいいと思います。





  1. 公式ドキュメントにはGitHubからcloneしてくる方法が記載されていますが、これは古い情報のようです。cloneしてきたリポジトリにZephirインストーラ(installファイル)が見当たりませんでした。 



  2. この辺りの話はこのpull requestを読むとわかりやすいです。 https://github.com/phalcon/zephir/pull/962