Edited at

BEAR.Sunday + Swoole


はじめに

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

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


phpbenchmarks.com

PHPのフレームワークのベンチマークサイトの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内部のベンチマークからの抜粋です。

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を並べ最低限必要なファイルを最初に一度に読み込むためです。

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

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

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

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


app

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

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

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


Swoole

以下は現在のBEAR.SundayでのSwoole実行の関数です。5

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では0コスト

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

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

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


パフォーマンス

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

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


Requests/sec: 110515.99

Requests/sec: 299103.32


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


  • Go ( default http server ): 116k req/s

  • PHP+Swoole: 132k req/s

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


ラスマスに言いたい

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

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

フレームワークが何故ダメかいくつか理由をあげますが、その1つ8


Frameworks Execute The Same Code Repeatedly Without Need

(フレームワークは必要なく繰り返し同じコードを実行する)


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


リンク





  1. これは「抽象化/多機能/"綺麗な"コード=遅い」 VS 「貧弱な抽象化/低機能/汚いコード=速い」のよくある単純な比較を超え、「優れたソフトウエアはパフォーマンスと品質を両立し、良い設計はパフォーマンスに寄与するはずである」という経験則、信条、それに少しばかりの希望からきています。 



  2. つまりほぼ同じと考えていいでしょう 



  3. http://www.phpbenchmarks.com/en/ 



  4. loadの91%が不要な訳ですから! 



  5. https://github.com/bearsunday/BEAR.Swoole/blob/master/bootstrap.php 



  6. PHP/Swoole outperformed the default Golang helloworld http server 



  7. しかもPHP Frameworks Dayというイベントでの発言 



  8. https://www.phpclasses.org/blog/post/226-4-Reasons-Why-All-PHP-Frameworks-Suck.html 



  9. https://gihyo.jp/news/report/2015/12/1401