PHP
jit
rfc
PHP8

PHP8でJITが使えるようになる

JITのRFCが2019/03/21に投票開始されました。

締切は2019/03/28ですが、2019/03/27時点で賛成48反対2でほぼ導入確定です。


JITとは


JIT is 何?

PHPは現在は、アクセスが来るたびにソースコードを全部読み取って、opcodeに変換して、順番に逐次実行して、実行が終了したら全てのコードを破棄するというインタプリタ型のプログラミング言語で、処理速度は遅いです。

遅いと言っても、やってる内容からすれば異常なまでに早いんですけどね。

opcodeはCPUやOSなどの実行環境によらず同一のコードが生成されます。

逐次実行するときはさらに実行環境ごとのネイティブコードに変換して実行されます。

OPcacheは、この変換後のopcodeをメモリに保存しておいて、次のリクエストでも使い回すという仕組みです。

JITはもう一段階進んだもので、リクエストが来たらソースコードを読んでopcodeにするまでは同じですが、その後一気にネイティブコードにまで変換してしまいます。

もう一度同じ処理が呼ばれたときはネイティブコードを直接実行することで、処理速度が非常に速くなります。

また、このネイティブコードをメモリに保存して使い回すこともできるようになります。

ただし、ネイティブコードはCPUが直接実行するコードなので、CPUの種類や世代によって異なるものとなります。

実行環境によって異なるネイティブコードを作らないといけないので、大規模な改修が必要で、コードも複雑になってたいへんです。

どのくらい大規模かというと、900コミット5万行の追加という目眩のする量です。


要するにどういうこと?

OPcacheのすごいやつ。


JIT RFC


Introduction

元々PHP7でJITを実装しようと企んでいて、2011年からZendが(ほとんどはDmitryが)色々と試していたんだけど、諸々の理由で結局PHP7に入ることはありませんでした。

理由はというと、当時の方法ではたいして早くならなかったこと、そのわりに複雑だったこと、そしてJIT以外にも多くのパフォーマンス向上技術の投入があったことです。

実際PHP7は、処理時間がPHP5の半分以下になるという驚異的な高速化がなされています。

当時はそれらの高速化に注力したため、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を使用します。

これはLuaJITプロジェクトで開発された、非常に軽量で高度なツールです。

しかし同時に、ターゲットのアセンブラ言語に対する高レベルの知識を要求します。

かつてLLVMを試してみたものの、コード生成速度は100倍遅く使い物になりませんでした。

DynAsmはPOSIXとWindows上でのx86とx86_64、およびARMをサポートしています。

従って、現在のPHPがサポートしている一般的なプラットフォーム全てに対応できるはずです。

努力すれば。

あと、ここで一部の内部実装について触れてるのですがよくわかりませんでした。

additional IR formに対応してないとか、opcacheオプティマイザのSSA静的解析フレームワークがネイティブコードを生成してるよとか、解析後long型になったらメモリじゃなくてCPUレジスタに直接登録するよとか、PHP JITのレジスタ割り付けアルゴリズムはすごいよとか、なんかそんなことが書いてあったりするようなないような。 

 ※詳細は@sj-i氏のコメント参照


パフォーマンス

以下の関数のベンチマークが公開されています。

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秒だったそうです。はえー。

実際に出力されたネイティブコード

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



後方互換性

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


その他の影響


エクステンション

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が0.320秒から0.140秒になりました。

CPUを多用する処理については、劇的な高速化が見込めます。

またNikitaによると、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には入りませんでした。

ブランチもできていたのですが、7.4への導入は賛成18反対34で却下されました。

7.4はただでさえ新機能盛り盛りで大変ですからね。

このうえさらに5万行の追加とか、さすがに厳しいでしょう。


外部リンク


プルリクエスト / JITブランチ

コミット数900、追加5万行を超える非常に大規模なプルリクです。

こんなマージ作業、自分じゃ絶対やりたくない。


PHP7.4ブランチ

PHP7.4用のJITブランチ。

こちらが使われることはなさそうです。


DynASM / 非公式DynASMドキュメント

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


[RFC] [VOTE] JIT

みんな『7.4は無理、8だけにすべき』というかんじです。

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


感想

普通にWebアプリを作っているかぎりにおいては、さしたる影響はないようです。

JITが真価を表すのは、バッチなどバックエンド処理においてでしょう。

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

なお、opcacheやそもそもJITについての理解があやふやなので、間違っている部分が多々あると思われます。

きっと誰かがプルリクしてくれるはず。