【PHP8.0】PHPでJITが使えるようになる

2020/06/26追記：アルファ版がリリースされたので[実際に試してみた](https://qiita.com/rana_kualu/items/73c346b3feadfa3a5b21)


[JITのRFC](https://wiki.php.net/rfc/jit)が2019/03/21に投票開始されました。
締切は2019/03/28ですが、2019/03/27時点で賛成48反対2でほぼ導入確定です。

# JITとは

## JIT is 何？

PHPは現在は、アクセスが来るたびにソースコードを全部読み取って、[opcode](https://www.php.net/manual/ja/internals2.opcodes.php)に変換して、順番に逐次実行して、実行が終了したら全てのコードを破棄するという[インタプリタ](https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF)型のプログラミング言語で、処理速度は遅いです。
遅いと言っても、やってる内容からすれば異常なまでに早いんですけどね。

opcodeはCPUやOSなどの実行環境によらず同一のコードが生成されます。
逐次実行するときはさらに実行環境ごとのネイティブコードに変換して実行されます。
[OPcache](https://www.php.net/manual/ja/book.opcache.php)は、この変換後のopcodeをメモリに保存しておいて、次のリクエストでも使い回すという仕組みです。

JITはもう一段階進んだもので、リクエストが来たらソースコードを読んでopcodeにするまでは同じですが、その後一気にネイティブコードにまで変換してしまいます。
もう一度同じ処理が呼ばれたときはネイティブコードを直接実行することで、処理速度が非常に速くなります。
また、このネイティブコードをメモリに保存して使い回すこともできるようになります。

ただし、ネイティブコードはCPUが直接実行するコードなので、CPUの種類や世代によって異なるものとなります。
実行環境によって異なるネイティブコードを作らないといけないので、大規模な改修が必要で、コードも複雑になってたいへんです。
どのくらい大規模かというと、[900コミット5万行の追加](https://github.com/php/php-src/pull/3792)という目眩のする量です。

## 要するにどういうこと？

OPcacheのすごいやつ。


# JIT RFC

## Introduction

元々PHP7でJITを実装しようと企んでいて、2011年からZendが(ほとんどは[Dmitry](https://github.com/dstogov)が)色々と試していたんだけど、諸々の理由で結局PHP7に入ることはありませんでした。
理由はというと、当時の方法ではたいして早くならなかったこと、そのわりに複雑だったこと、そしてJIT以外にも多くのパフォーマンス向上技術の投入があったことです。

実際PHP7は、処理時間がPHP5の半分以下になるという[驚異的な高速化](https://qiita.com/rana_kualu/items/05a510f53c0ea9a1ccaf)がなされています。
当時はそれらの高速化に注力したため、JITの導入は見送られました。


## The Case for JIT Today

現在のPHPにおけるJITの導入には、以下のようなメリットが見込まれます。

まず、JIT以外の最適化戦略による高速化は、そろそろ限界に達しつつあります。
つまり、JITを使わないかぎり、これ以上の高速化は見込めません。

次に、JITを導入することによって、Web以外のCPUを多用するような処理をPHPで書く、という選択肢が有力なものになります。

最後に、C言語ではなく、もしくはC言語のかわりに、PHPで組み込み関数を開発することが可能になります。
現在のPHPでそのような戦略を採るには大きな壁となっている、パフォーマンス劣化という問題の影響をほとんど受けなくなります。
さらにPHPベースで開発すれば、C言語ベースでの開発では往々にして発生するメモリ管理、オーバーフローといった問題を、言語レベルで安全にしてくれます。


## Proposal

PHP8でJITを提供します。

PHP JITはOPcacheの一部として、しかしほぼ独立したものとして実装されます。
PHPのコンパイル時に有効無効を設定します。
有効にした場合、PHPファイルのネイティブコードがOPCacheの共有メモリに保存されるようになります。

ネイティブコードの生成には[DynAsm](https://luajit.org/dynasm.html)を使用します。
これはLuaJITプロジェクトで開発された、非常に軽量で高度なツールです。
しかし同時に、ターゲットのアセンブラ言語に対する高レベルの知識を要求します。
かつてLLVMを試してみたものの、コード生成速度は100倍遅く使い物になりませんでした。
DynAsmはPOSIXとWindows上でのx86とx86_64、およびARMをサポートしています。
従って、現在のPHPがサポートしている一般的なプラットフォーム全てに対応できるはずです。
努力すれば。

あと、ここで一部の内部実装について触れてるのですがよくわかりませんでした。
additional IR formに対応してないとか、opcacheオプティマイザのSSA静的解析フレームワークがネイティブコードを生成してるよとか、解析後long型になったらメモリじゃなくてCPUレジスタに直接登録するよとか、PHP JITのレジスタ割り付けアルゴリズムはすごいよとか、なんかそんなことが書いてあったりするようなないような。　
　※詳細は@sj-i氏のコメント参照


## パフォーマンス

以下の関数のベンチマークが[公開](https://gist.github.com/dstogov/12323ad13d3240aee8f1)されています。

```php
function iterate($x,$y){
    $cr = $y-0.5;
    $ci = $x;
    $zr = 0.0;
    $zi = 0.0;
    $i = 0;
    while (true) {
        $i++;
        $temp = $zr * $zi;
        $zr2 = $zr * $zr;
        $zi2 = $zi * $zi;
        $zr = $zr2 - $zi2 + $cr;
        $zi = $temp + $temp + $ci;
        if ($zi2 + $zr2 > BAILOUT)
            return $i;
        if ($i > MAX_ITERATIONS)
            return 0;
    }
}
```

実行結果は以下のとおり。

| 環境 | 実行時間 |
|----|---- |
| PHP7-JIT (JIT=on) | 0.011 |
| gcc -O2 (4.9.2) | 0.013 |
| LuaJIT-2.0.3 (JIT=on)  | 0.014 |
| gcc -O0 (4.9.2) | 0.022 |
| HHVM-3.5.0 (JIT=on)  | 0.030 |
| Java-1.8.0 (JIT=on) | 0.059 |
| LuaJIT-2.0.3 (JIT=off) | 0.073 |
| Java-1.8.0 (JIT=off) | 0.251 |
| PHP-7 | 0.281 |
| squirrel-3.0.4 | 0.335 |
| Lua-5.2.2 | 0.339 |
| PHP-5.6 | 0.379 |
| PHP-5.5 | 0.383 |
| PHP-5.4 | 0.406 |
| ruby-2.1.5 | 0.684 |
| PHP-5.3 | 0.855 |
| HHVM-3.5.0 (JIT=off) | 0.978 |
| PHP-5.2 | 1.096 |
| python-2.7.8 | 1.128 |
| PHP-5.1 | 1.217 |
| perl-5.18.4 | 2.083 |
| PHP-4.4 | 4.209 |
| PHP-5.0 | 4.434

バージョンは不明ですが素のPHP7の20倍以上、HHVMの3倍、そしてgccやLuaJITより速い。
さすがに何か間違ってるんじゃないか？と首を傾げる結果です。

なお、このベンチは4年前のものです。
RFCによるとJITなしのPHP7.4では0.046秒ということでした。
PHP7.4では0.046秒で、PHP7では0.281秒って、既にこの時点でおかしい気がするぞ？
単に実行環境の違いでしょうか。

あと、よく見ると、PHPだけob_start/ob_end_flushを使って出力を抑制してるのが気になりますね。
PHP同士での比較には影響ありませんが、他言語との比較はフェアでないと思われます。

ちなみにGoで並列処理したら0.002秒だったそうです。はえー。

<details><summary>実際に出力されたネイティブコード</summary><div>

```
JIT$Mandelbrot::iterate: ; (/home/dmitry/php/bench/b.php)
	sub $0x10, %esp
	cmp $0x1, 0x1c(%esi)
	jb .L14
	jmp .L1
.ENTRY1:
	sub $0x10, %esp
.L1:
	cmp $0x2, 0x1c(%esi)
	jb .L15
	mov $0xec3800f0, %edi
	jmp .L2
.ENTRY2:
	sub $0x10, %esp
.L2:
	cmp $0x5, 0x48(%esi)
	jnz .L16
	vmovsd 0x40(%esi), %xmm1
	vsubsd 0xec380068, %xmm1, %xmm1
.L3:
	mov 0x30(%esi), %eax
	mov 0x34(%esi), %edx
	mov %eax, 0x60(%esi)
	mov %edx, 0x64(%esi)
	mov 0x38(%esi), %edx
	mov %edx, 0x68(%esi)
	test $0x1, %dh
	jz .L4
	add $0x1, (%eax)
.L4:
	vxorps %xmm2, %xmm2, %xmm2
	vxorps %xmm3, %xmm3, %xmm3
	xor %edx, %edx
.L5:
	cmp $0x0, EG(vm_interrupt)
	jnz .L18
	add $0x1, %edx
	vmulsd %xmm3, %xmm2, %xmm4
	vmulsd %xmm2, %xmm2, %xmm5
	vmulsd %xmm3, %xmm3, %xmm6
	vsubsd %xmm6, %xmm5, %xmm7
	vaddsd %xmm7, %xmm1, %xmm2
	vaddsd %xmm4, %xmm4, %xmm4
	cmp $0x5, 0x68(%esi)
	jnz .L19
	vaddsd 0x60(%esi), %xmm4, %xmm3
.L6:
	vaddsd %xmm5, %xmm6, %xmm6
	vucomisd 0xec3800a8, %xmm6
	jp .L13
	jbe .L13
	mov 0x8(%esi), %ecx
	test %ecx, %ecx
	jz .L7
	mov %edx, (%ecx)
	mov $0x4, 0x8(%ecx)
.L7:
	test $0x1, 0x39(%esi)
	jnz .L21
.L8:
	test $0x1, 0x49(%esi)
	jnz .L23
.L9:
	test $0x1, 0x69(%esi)
	jnz .L25
.L10:
	movzx 0x1a(%esi), %ecx
	test $0x496, %ecx
	jnz JIT$$leave_function
	mov 0x20(%esi), %eax
	mov %eax, EG(current_execute_data)
	test $0x40, %ecx
	jz .L12
	mov 0x10(%esi), %eax
	sub $0x1, (%eax)
	jnz .L11
	mov %eax, %ecx
	call zend_objects_store_del
	jmp .L12
.L11:
	mov 0x4(%eax), %ecx
	and $0xfffffc10, %ecx
	cmp $0x10, %ecx
	jnz .L12
	mov %eax, %ecx
	call gc_possible_root
.L12:
	mov %esi, EG(vm_stack_top)
	mov 0x20(%esi), %esi
	cmp $0x0, EG(exception)
	mov (%esi), %edi
	jnz JIT$$leave_throw
	add $0x1c, %edi
	add $0x10, %esp
	jmp (%edi)
.L13:
	cmp $0x3e8, %edx
	jle .L5
	mov 0x8(%esi), %ecx
	test %ecx, %ecx
	jz .L7
	mov $0x0, (%ecx)
	mov $0x4, 0x8(%ecx)
	jmp .L7
.L14:
	mov %edi, (%esi)
	mov %esi, %ecx
	call zend_missing_arg_error
	jmp JIT$$exception_handler
.L15:
	mov %edi, (%esi)
	mov %esi, %ecx
	call zend_missing_arg_error
	jmp JIT$$exception_handler
.L16:
	cmp $0x4, 0x48(%esi)
	jnz .L17
	vcvtsi2sd 0x40(%esi), %xmm1, %xmm1
	vsubsd 0xec380068, %xmm1, %xmm1
	jmp .L3
.L17:
	mov %edi, (%esi)
	lea 0x50(%esi), %ecx
	lea 0x40(%esi), %edx
	sub $0xc, %esp
	push $0xec380068
	call sub_function
	add $0xc, %esp
	cmp $0x0, EG(exception)
	jnz JIT$$exception_handler
	vmovsd 0x50(%esi), %xmm1
	jmp .L3
.L18:
	mov $0xec38017c, %edi
	jmp JIT$$interrupt_handler
.L19:
	cmp $0x4, 0x68(%esi)
	jnz .L20
	vcvtsi2sd 0x60(%esi), %xmm3, %xmm3
	vaddsd %xmm4, %xmm3, %xmm3
	jmp .L6
.L20:
	mov $0xec380240, (%esi)
	lea 0x80(%esi), %ecx
	vmovsd %xmm4, 0xe0(%esi)
	mov $0x5, 0xe8(%esi)
	lea 0xe0(%esi), %edx
	sub $0xc, %esp
	lea 0x60(%esi), %eax
	push %eax
	call add_function
	add $0xc, %esp
	cmp $0x0, EG(exception)
	jnz JIT$$exception_handler
	vmovsd 0x80(%esi), %xmm3
	jmp .L6
.L21:
	mov 0x30(%esi), %ecx
	sub $0x1, (%ecx)
	jnz .L22
	mov $0x1, 0x38(%esi)
	mov $0xec3802b0, (%esi)
	call rc_dtor_func
	jmp .L8
.L22:
	mov 0x4(%ecx), %eax
	and $0xfffffc10, %eax
	cmp $0x10, %eax
	jnz .L8
	call gc_possible_root
	jmp .L8
.L23:
	mov 0x40(%esi), %ecx
	sub $0x1, (%ecx)
	jnz .L24
	mov $0x1, 0x48(%esi)
	mov $0xec3802b0, (%esi)
	call rc_dtor_func
	jmp .L9
.L24:
	mov 0x4(%ecx), %eax
	and $0xfffffc10, %eax
	cmp $0x10, %eax
	jnz .L9
	call gc_possible_root
	jmp .L9
.L25:
	mov 0x60(%esi), %ecx
	sub $0x1, (%ecx)
	jnz .L26
	mov $0x1, 0x68(%esi)
	mov $0xec3802b0, (%esi)
	call rc_dtor_func
	jmp .L10
.L26:
	mov 0x4(%ecx), %eax
	and $0xfffffc10, %eax
	cmp $0x10, %eax
	jnz .L10
	call gc_possible_root
	jmp .L10
```

</div></details>


## 後方互換性

互換性の壊れる変更はありません。


## その他の影響

### エクステンション

Xdebugのようなデバッガ、XHProf、Blackfire、Tidewaysといったプロファイラに影響が発生します。

### Opcache

JITはOpcacheの一機能として実装されます。

### 追加される定数

追加される定数はありません。

### デバッグ

JITのデバッグはとっても大変だよ！がんばれ！


## php.ini

php.iniに複数の項目が追加されます。

### opcache.jit_buffer_size

ネイティブコードのために予約するメモリサイズ。バイト単位で、K・Mの表記に対応。
デフォルトは0で、JIT無効という意味。

### opcache.jit

JITの制御オプション。順番にCRTOを表し、デフォルトは"1205"。
おそらく"1235"に変更した方がいいかもしれない。

#### C

CPU最適化レベル、範囲は0-1。
0は使用しない、1はAVX命令セットを有効にする。

#### R

レジスタ割り当て、範囲は0-2。
0はレジスタ割り当てを使用しない、1はローカルレジスタ割り付け、2はグローバルレジスタ割り付け。

#### T

JITを起動するタイミング、範囲は0-5。
0は最初のスクリプト起動時に全機能を有効にする。
1は最初の処理実行時にJITを有効にする。
2は最初のリクエストでプロファイルを行い、2回目のリクエストでコンパイルする。
3はオンザフライでプロファイル、コンパイルを行う。
4は`@jit`ってコメントが書いてある関数をコンパイルする。

#### O

最適化レベル、範囲は0-5。
0はJITを使わない、5が最も高度な最適化を行う。

### opcache.jit_debug

JITデバッグ制御オプション。
デフォルトは0。
それぞれビット指定することで各種デバッグ情報を出力できるみたいですが、具体的に何が何なのかはよくわかりませんでした。
SSA formとかperf.mapとかJIt-ed codeとか出せるらしい。


## パフォーマンス

[bench.php](https://github.com/php/php-src/blob/master/Zend/bench.php)が0.320秒から0.140秒になりました。
CPUを多用する処理については、劇的な高速化が見込めます。
また[Nikita](https://github.com/nikic)によると、PHP-Parserが1.3倍速くなりました。

しかしながら、WordPressのようなWebアプリについては、さほど恩恵は見込めません。
315req/秒から326req/秒になった程度のようです。
このような現実的アプリについても高速化の改善を行う、追加の取り組みを実施予定です。


## 今後の展望

関数のプロファイリング後に最適化されたコードを生成することで、JITの改善を行う予定です。
またプリローディングやFFIとのより深い統合を行うことができるでしょう。
CではなくPHPで書かれた組み込み関数を提供する方法の標準化も見込めます。

## 投票

2019/03/21に投票開始、2019/03/28に投票終了。
可決には投票者の2/3+1の賛成が必要です。


## PHP7.4

PHP7.4には入りませんでした。
[ブランチ](https://github.com/zendtech/php-src/tree/jit-dynasm-7.4)もできていたのですが、7.4への導入は賛成18反対34で却下されました。
7.4はただでさえ[新機能盛り盛り](https://qiita.com/rana_kualu/items/38c9917f634bc7767f1b)で大変ですからね。
このうえさらに5万行の追加とか、さすがに厳しいでしょう。

## 外部リンク

### [プルリクエスト](https://github.com/php/php-src/pull/3792) / [JITブランチ](https://github.com/zendtech/php-src/)

コミット数900、追加5万行を超える非常に大規模なプルリクです。
こんなマージ作業、自分じゃ絶対やりたくない。

### [PHP7.4ブランチ](https://github.com/zendtech/php-src/tree/jit-dynasm-7.4)

PHP7.4用のJITブランチ。
こちらが使われることはなさそうです。

### [DynASM](https://luajit.org/dynasm.html) / [非公式DynASMドキュメント](https://corsix.github.io/dynasm-doc/)

JITで使われるライブラリです。

### [[RFC] [VOTE] JIT](https://externals.io/message/104842)

みんな『7.4は無理、8だけにすべき』というかんじです。
これだけ大規模な改修にもかかわらず、スレッドが意外と伸びてないのは、もはや対象バージョン以外語ることがないくらい予定調和だからでしょうか。


# 感想

普通にWebアプリを作っているかぎりにおいては、さしたる影響はないようです。
JITが真価を表すのは、バッチなどバックエンド処理においてでしょう。

一昔前であれば『PHPでバッチ？正気か！？』というイメージでしたが、今後はもはや下手な言語で書くよりPHPのほうが速い、までありそうですね。


なお、opcacheやそもそもJITについての理解があやふやなので、間違っている部分が多々あると思われます。
きっと誰かがプルリクしてくれるはず。