LoginSignup
262

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-03-27

2020/06/26追記:アルファ版がリリースされたので実際に試してみた

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についての理解があやふやなので、間違っている部分が多々あると思われます。
きっと誰かがプルリクしてくれるはず。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
262