PHP7.4のpreloadいれたらLaravelは早くなるのだろうかと思って検証した

皆さんこんにちは

PHP7.4でpreloadが来るっていうので、ワクテカしているわけですが、実際どれくらい早くなるのかしらって思いまして、ちょうど色々検証していたので、Laravelでも軽くやっちゃおうぜって思ったので、やってみます。

あと4日ですね！！

# 三行で

- preloadが来るよ
- preloadで読み込み済みの状態を作れるよ
- Laravelに導入すると、結構早くなるよ

# Preload

PHP7.4でopcacheにpreloadという機能が入ります。これの概要は

- opcacheの設定にpreloadを実行するヘルパーファイルを指定できる
- webサーバを起動するとopcacheがヘルパーファイルを実行する
- ヘルパーファイル内でキャッシュ化したものは、通常のアクセスにおいて、**すでに読み込まれたものとしてみなされる**

そう、読み込み済みですよ！
簡単に読み込み済みの効果を見るために、以下のような例を考えてみましょう。

## preload利用の例

まずは、こんなDockerfileで、動かせるコンテナを作ってみます。

```Dockerfile
FROM php:7.4.0RC4-cli

RUN apt update && apt install -y vim sudo && docker-php-ext-install opcache &&\
    echo 'opcache.preload=/var/www/preload.php' >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini &&\
    useradd nginx

WORKDIR /var/www
```
ベースコンテナはちょいと古いのですが、まあ、検証当時のものなんで、許してくださいな。
そして、以下のようなディレクトリ構成のファイル群を用意します。

```
classes
 - A.php
 - B.php
index.php
preload.php
```
内容としては

```A.php
<?php 
class A
{
    public function name(): string
    {
        return return static::class;
    }
}
```

```B.php
<?php
class B extends A {}
```

```index.php
<?php
$b = new B;

echo $b->name() . '<BR>';
```
さて、サーバのエンドポイントとしてはindex.phpを使うわけですが、この中ではrequireを一回も使っていないし、当然autoloadの設定もしていないので、

```
sudo -u nginx php -S 0.0.0.0:8080
```
としてサーバを起動しても、いざアクセスすると「Bなんてクラス知らんのだけど」っていうエラーが出るだけです。

ここで、preloadを使ってみましょう。
先程のDockerfileには予めopcache.preloadの設定が挿入されるようになっています。実際に書かれている設定はこんな感じになっています。

```/usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/opcache.so
opcache.preload=/var/www/preload.php
```
この`opcache.preload`がヘルパーファイルの場所になっているので、そこに以下のようなヘルパーをおいておきましょう。

```preload.php
<?php

$files = glob(__DIR__ . '/classes/**.php');

foreach ($files as $file) {
    opcache_compile_file($file);
}
```
この状態でサーバを起動します。

```
sudo -u nginx php -S 0.0.0.0:8080
[Sun Nov 24 04:54:36 2019] PHP 7.4.0RC4 Development Server (http://0.0.0.0:8080) started
```
ちゃんと起動したようです。
それではアクセスしてみましょう。

```
$ curl http://localhost:8080
B<BR>
```
普通に動きますね。

# preloadは性能に絡めるのか
多くのWebフレームワークは大量のクラスが定義されていますが、PHPでそれを読むにはいちいちファイルを見に行かなければなりません。しかも、PHPでは、基本的に各アクセスごとにプロセスが独立しているため、ファイルの読み込みもアクセスごとに発生します。
opcacheでこの動きを緩和することができますが、その動きは割と複雑です。

1. クラスが呼ばれる (静的・インスタンス生成など)
2. クラスの定義がない場合、autoloadで設定したルールに従い、ファイルをrequireする
3. requireしようとしたファイルのキャッシュがあった場合、ファイルのタイムスタンプよりも新しければ、そのままキャッシュを使う
4. キャッシュが古ければ、ファイルを読みに行く(設定によってスキップ可能)
5. ファイルを読んだ場合はキャッシュを更新する

preloadしてあれば、1ですむので、それなりに早くなるんじゃないかって思います。

## Laravelに入れてみる
たくさんのファイルを読み込むといえばLaravelですね！
そこで、適当にLaravelを入れて、検証してみましょう。

### includeしているファイルを見てみる

とりあえず、どのくらいのファイルを読み込んでいるのか見てみます。

```public/index.php
$kernel->terminate($request, $response);

$includes = get_included_files();

file_put_contents('/var/www/includes.txt', implode("\n", $includes));
```
こんな感じのを仕込むと、ファイルのリストが出てきます。350ファイル以上ありますね。

### preloadに入れてみる
適当にpreloadに入れるわけですが、viewのキャッシュファイルとかは読んでもしょうがないかなとか思いながら、以下のようなloaderを作ります。

```preload.php
<?php
$includes = file_get_contents('/var/www/includes.txt');
$includes = explode("\n", $includes);

foreach ($includes as $include) {
    if (strpos($include, '/storage/framework') !== false) {
        echo $include, " - storage \n";
        continue;
    }

    if (strpos($include, 'index.php') !== false) {
        echo $include, " - index \n";
        continue;
    }

    if (strpos($include, 'src/config') !== false) {
        echo $include, " - config\n";
        continue;
    }

    opcache_compile_file($include);
}
```
こいつをpreloadした状態でサーバを起動します。
index.phpに`error_log(count($includes));`みたいなコードを突っ込んでやると、読み込みファイル数が363 -> 46 になりました。せっかくなら1とかになってくれるとありがたかったんですが、そううまくは行かないようですね。

### 検証する
例によってab使って一分間のアクセスレートを見てみましょう。
abはubuntu 18.04 (wsl2のやつ) を使ってます。

#### preloadなし

```
$ ab -t 60 -c 1 http://localhost:8080/

...

Concurrency Level:      1
Time taken for tests:   60.004 seconds
Complete requests:      23
Failed requests:        0
Total transferred:      77747 bytes
HTML transferred:       55798 bytes
Requests per second:    0.38 [#/sec] (mean)
Time per request:       2608.884 [ms] (mean)
Time per request:       2608.884 [ms] (mean, across all concurrent requests)
Transfer rate:          1.27 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       0
Processing:   177 2609 1619.2   2894    7586
Waiting:      176 2608 1619.1   2893    7585
Total:        177 2609 1619.2   2894    7586

Percentage of the requests served within a certain time (ms)
  50%   2844
  66%   3064
  75%   3230
  80%   3256
  90%   3323
  95%   3658
  98%   7586
  99%   7586
 100%   7586 (longest request)
```
2.5秒超えるのか。。。
一応、opecache使ってファイルはキャッシュしているはずなんですが。

#### preloadあり

```
$ ab -t 60 -c 1 http://localhost:8080/

...

Concurrency Level:      1
Time taken for tests:   60.160 seconds
Complete requests:      301
Failed requests:        0
Total transferred:      1017565 bytes
HTML transferred:       730226 bytes
Requests per second:    5.00 [#/sec] (mean)
Time per request:       199.867 [ms] (mean)
Time per request:       199.867 [ms] (mean, across all concurrent requests)
Transfer rate:          16.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   148  200 140.9    179    1575
Waiting:      148  199 140.9    179    1574
Total:        148  200 140.9    179    1575

Percentage of the requests served within a certain time (ms)
  50%    179
  66%    187
  75%    193
  80%    199
  90%    209
  95%    224
  98%    260
  99%    852
 100%   1575 (longest request)
```
ほんまか？
10倍強の速度になっているんだが。

# まとめ
速度強化が劇的すぎて、流石にコンテナ特有のボリュームアクセス遅い問題に関係しちゃっているのかもしれないなぁとか思いながらも、ファイルアクセスが減って早くなっているのは事実だなぁって思いました。
将来的にはフレームワークごとにpreloadのヘルパーを用意してくれると、こっちとしては脳死で実装できて楽だなぁって他力本願に考えちゃったりしています。

今回はこんなところです。