BEAR.Sunday + Swoole

# はじめに

BEAR.Sundayは開発初期からパフォーマンスを重視してきました。[^1]

何がフレームワークの実行を遅くしているかを分析し、設計と実装でパフォーマンスの向上に取り組んできました。

## phpbenchmarks.com

PHPのフレームワークのベンチマークサイトの[phpbenchmarks.com](http://www.phpbenchmarks.com/)によれば**ミニマムブートストラップにおいて**、Laravel 5.7はCakePHP 3.7に対して約5%速く[^2]、Symfony 4.2はLaravel 5.7に対して122%高速とのスコアを出しています。[^3]

現在のBEAR.SundayはそのSymfony 4.2に対して約50%高速です。しかしSwooleを使うとそのまた約6倍の速度で動作します。

## なぜ速いのか？

Swooleで何故そんなに速くなるのかを検討する前に、現状でフレームワークのどの部分に実行コストがかかっているか見てみましょう。以下は去年のアドベントカレンダーの記事、[BEAR.Sunday内部のベンチマーク](https://qiita.com/koriym/items/3b8c1cf362a973f808bd)からの抜粋です。

|action| 経過時間 |所要時間 | 割合 |
|------|---------|--------|------|
| load | 6.490 | 6.490 | 91.3% |
| app | 6.728 | 0.239 | 3.4% |
| route | 6.791 | 0.062 | 0.9% |
| request | 7.097 | 0.306 | 4.3% |
| transfer | 7.109 | 0.011 | 0.2% |

実際に実行する部分(route, request, transfer)は初期化(load, app)に比べて随分小さいのが分かります。
つまりフレームワークのミニマムブートストラップのかなりの部分が初期化コストです。クラスを読み込む`load`とアプリケーション実行のためのオブジェクトを用意する`app`を分けてみてみましょう。

### load

`load`すなわちPHPファイルの`require`で全体時間の91.3%もかかっています。BEAR.Sundayの`autoload.php`は通常の`vendor/autoload.php`と違い、事前に　コンパイルし`require`を並べ最低限必要なファイルを最初に一度に読み込むためです。

このやり方は`--optimize`で`dump-autoload`された`autoload.php`よりもずっと高速です。

多くのフレームワークではこういうアプローチは取らずに、未知のクラスが出現した時点で [spl_autoload_register](http://php.net/manual/ja/function.spl-autoload-register.php)で登録されたautoloadのための関数が都度呼ばれ実行されます。つまり`require`の単体のコストはそれぞれのオブジェクトの実行に隠れていて分かりにくく、しかし実はコストはかなり大きいのです。

Phalconが高速なのは「Cで実装されているから」と説明されますが、それもそうなのですが「PHPがスタートした段階でフレームワーククラスの読み込みコストが無い」事が大きな要因である事と推測します。[^4]

PHP7.4で登場予定の [_preload](https://wiki.php.net/rfc/preload)はこの事情を一変させます。リクエストを超えてPHPファイルの読み込みが再利用されるので、BEAR.SundayのautoloadやPhalconが持っているアドバンテージは無くなっていくでしょう。

### app

`app`はアプリケーションのオブジェクトグラフを生成しているところです。BEAR.Sundayはフレームワーク実行に必要なオブジェクトはルートオブジェクト(`$app`)１つのみで、他の必要なコンポーネント(ルーターやリクエストクライアント）はそのpublicプロパティです。

 * `$app`のオブジェクトグラフ http://koriym.github.io/print_o/v1/libs/bear.sunday.html

他のフレームワークではconfigの通りにオブジェクトコンテナ（サービスコンテナ）にセットしている"コンテナの初期化部分"にもあたります。コードを生成したり設定値のキャッシュを使ったりしてコスト削減していますが、BEAR.Sundayでは基本的に`unserialize($cached_app)`とアンシリアライズしているだけです。ここの部分も大きくパフォーマンスに寄与しています。

（この実装については、doctrine2,zendframeworkチームのメンバーでもあるマリオのアイコンでおなじみのスーパーエンジニア[@Ocramius](https://twitter.com/Ocramius)氏にこのようなコメントをもらいました。）

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr"><a href="https://twitter.com/koriym?ref_src=twsrc%5Etfw">@koriym</a> your idea of serializing the app after bootstrap is just fantastic! :D</p>&mdash; Core Domme, Sub Domain (@Ocramius) <a href="https://twitter.com/Ocramius/status/583060125973385217?ref_src=twsrc%5Etfw">2015年4月1日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



## Swoole

以下は現在のBEAR.SundayでのSwoole実行の関数です。[^9]

```php
return function (
    string $context,
    string $name,
    string $ip,
    int $port,
    int $mode = SWOOLE_BASE,
    int $sockType = SWOOLE_SOCK_TCP,
    array $settings = ['worker_num' => 4]
) : int {
    $http = new Server($ip, $port, $mode, $sockType);
    $http->set($settings);
    $http->on('start', function () use ($ip, $port) {
        echo "Swoole http server is started at http://{$ip}:{$port}" . PHP_EOL;
    });
    $injector = new AppInjector($name, $context);
    /* @var App $app */
    $app = $injector->getOverrideInstance(new Psr7SwooleModule, App::class);
    $superGlobals = new SuperGlobals;
    $http->on('request', function (Request $request, Response $response) use ($app, $superGlobals) {
        if ($app->httpCache->isNotModified($request->header)) {
            $app->httpCache->transfer($response);

            return;
        }
        $superGlobals($request);
        $match = $app->router->match($GLOBALS, $_SERVER);
        try {
            /* @var ResourceObject $ro */
            $ro = $app->resource->{$match->method}->uri($match->path)($match->query);
            $app->responder->setResponse($response);
            $ro->transfer($app->responder, []);
        } catch (\Exception $e) {
            $app->error->transfer($e, $request, $response);
        }
    });
    $http->start();
};
```

## Swooleでは０コスト

一度起動したSwooleのスクリプトは終了する事なく`$http->on('request'...` で次のリクエストを待ちます。つまり上記で説明した`load`と`app`のコストは0になり、PHP 7.4の[_preload](https://wiki.php.net/rfc/preload)は必要ありません。`$app`はイミュータブルなので再初期化不要でそのまま使えます。

つまり90%以上を占めていた「初期化コストが丸々消失する」のがBEAR.Sundayでのパフォーマンス向上の最大要因です。初期化が十分整理されていれば他のフレームワークでもこの程度のパフォーマンス向上が見込めるのではないでしょうか。

また、通常はアノテーションなどサーバー間で共有する必要のないキャッシュにはAPCキャッシュを使っていますが、SwooleではArrayキャッシュを使います。PHPの変数は通常リクエスト毎にリセットされますがSwooleだと消えないため単なるArray変数をキャッシュに使うのが最速です。

## パフォーマンス

SwooleはPHPのパフォーマンス向上における真のゲームチェンジャーです。強烈な速度向上はFramework XはFramework Yより50%速いといった比較を吹き飛ばします。PHP7.4の`_preload`を待つ事なく、Cで書かれたフレームワークを使う事なく、リクエスト毎の巨大なクラス読み込みコストを０にして、ブートストラップのための初期化コストを激減させます。

Swooleの公式ベンチマーク https://www.swoole.co.uk/benchmark もご覧ください

> Requests/sec: 110515.99
> Requests/sec: 299103.32

https://github.com/swoole/swoole-src/issues/1401 にはGoとの比較もあります。[^8]

 * Go ( default http server ): 116k req/s
 * PHP+Swoole: 132k req/s

この記事ではフレームワークでの実装経験と、過去のフレームワ内部のベンチマークからSwooleがどのようにパフォーマンスに寄与しているか考察しました。

## ラスマスに言いたい

PHPの作者、Rasmus Lerdorf氏の伝説的なコメントに「PHP Frameworks all suck !」  というのがあります。[^6]

https://www.youtube.com/watch?v=DuB6UjEsY_Y

フレームワークが何故ダメかいくつか理由をあげますが、その１つ[^5]

> Frameworks Execute The Same Code Repeatedly Without Need
（フレームワークは必要なく繰り返し同じコードを実行する）

2015年にインタビューでRasmusさんとお会いしましたが[^7]、次に機会があれば「それは解決しそうですよ！」と言いたいです。

## リンク
 
 * BEAR.Sunday http://bearsunday.github.io/manuals/1.0/ja/swoole.html
 * Laravel https://github.com/swooletw/laravel-swoole/wiki/1.-Introduction
 * Zend Expressive https://framework.zend.com/blog/2018-03-21-expressive-swoole.html
 * Symfony？ https://packagist.org/?query=symfony%20swoole 

[^1]: これは「抽象化/多機能/"綺麗な"コード=遅い」 VS 「貧弱な抽象化/低機能/汚いコード=速い」のよくある単純な比較を超え、「優れたソフトウエアはパフォーマンスと品質を両立し、良い設計はパフォーマンスに寄与するはずである」という経験則、信条、それに少しばかりの希望からきています。
[^2]: つまりほぼ同じと考えていいでしょう
[^3]: http://www.phpbenchmarks.com/en/
[^4]: loadの91%が不要な訳ですから！
[^5]: https://www.phpclasses.org/blog/post/226-4-Reasons-Why-All-PHP-Frameworks-Suck.html
[^6]: しかもPHP Frameworks Dayというイベントでの発言
[^7]: https://gihyo.jp/news/report/2015/12/1401
[^8]: [PHP/Swoole outperformed the default Golang helloworld http server](https://www.reddit.com/r/PHP/comments/86bkc4/phpswoole_outperformed_the_default_golang/)
[^9]: https://github.com/bearsunday/BEAR.Swoole/blob/master/bootstrap.php