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

# TL;DR

- Zephirを使ってPHPの拡張モジュールを作成・テストする方法について書いています
- 任意の整数$n$に対してフィボナッチ数列$F_{n}$を求めるプログラム（再帰版）を使ってPHPとのパフォーマンスを比較したところ、$F_{38}$ではZephirの方がPHPより約19倍高速になりました

# 動作確認環境

この記事の動作確認環境は次の通りです。

<dl>
  <dt>OS</dt>
  <dd>macOS High Sierra 10.13.6</dd>
  <dt>PHP</dt>
  <dd>7.2.10（phpenvでインストール）</dd>
  <dt>Zephir</dt>
  <dd>0.11.8</dd>
  <dt>Zephir Parser</dt>
  <dd>1.1.3</dd>
</dl>

# コード

この記事で使ったコードはGitHubに公開しています。

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

# Zephirとは？

[Zephir](https://docs.zephir-lang.com/)（<strong>Ze</strong>nd Engine <strong>PH</strong>P <strong>I</strong>nte<strong>r</strong>mediate）とは、PHPの拡張モジュールの作成を容易にすることを目的として開発されたプログラミング言語です。

Zephirで作成したプログラムはC言語のコードに変換され、C言語のコンパイラによってコンパイルされます。そのためPHPより高速に動作します。
また、C言語と違いポインタや直接的なメモリ操作が許されていないので、直接C言語で書くより簡単に拡張モジュールを書くことができます。

Zephirは動的型と静的型の両方を持ちます。
変数に型宣言を書くことで、PHPより型安全にプログラムを書くことができます。
文法はPHPに似ており、PHPを書いたことがある人であればすぐに馴染めると思います。
PHPの組み込み関数を使用することもできます。

# インストール

Composerを使ってインストールできます[^1]。

```
$ composer global require phalcon/zephir
```

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

`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](https://github.com/phalcon/php-zephir-parser)もインストールしておきます。

```
$ git clone git://github.com/phalcon/php-zephir-parser.git
$ cd php-zephir-parser
$ phpize
$ ./configure
$ make
$ sudo make install
```

インストール後、`php.ini`に拡張モジュールを追加します。

```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のプログラムを作成します。

```php: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:Makefile
build:
	zephir fullclean
	zephir build
```

`make build`コマンドの実行前に、`config.json`の`internal-call-transformation`を`true`に変更しておきます。

```json:config.json
"internal-call-transformation": true
```

`false`のままだと、全てのメソッド呼び出しがPHPスクリプトを介して行われてしまうので、今回のフィボナッチ数列を求めるプログラムのように再帰呼び出しが非常に多い場合、パフォーマンスが出ません。
試しに`false`のまま`make build`コマンドを実行して拡張モジュールを生成してみたところ、PHPと同程度かやや遅いくらいのパフォーマンスしか出ませんでした。
`true`にすると、内部的なメソッド呼び出しがPHPスクリプトを介さずに行われるようになるので、その分高速になります[^2]。

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

`make build`コマンドを実行します。

```
$ make build
```

生成された拡張モジュールは拡張モジュールのディレクトリ（`extension_dir`に指定されたディレクトリ）に配置されるので、`php.ini`に追加するだけでPHPから使用できるようになります。

```ini
[My Zephir]
extension=myzephir.so
```

次のようにして、PHPから作成した拡張モジュールのメソッドを呼び出すことができます。

```php
MyZephir\Fibonacci::fib(10); # 55
```

# ユニットテスト

ユニットテストは通常のPHPプログラムと同じように、PHPUnitからテストしたい拡張モジュールのメソッドを呼び出すことで行えます。

```php
<?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
<?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で拡張モジュール化することで、処理の高速化を実現できそうです。
ただし、ビルドやデプロイなどの考慮が必要になること、テストのカバレッジが計測できなくなることといったデメリットもあります。
メリットとデメリットのトレードオフを見極めた上で使用するかどうかを決めた方がいいと思います。