119 Views
April 16, 26
スライド概要
PHPコードが実行されるまでの仕組みを追いながら、OpcacheとJITがそれぞれ何を最適化しているのかを解説します。
は何を"翻訳"しているのか PHP 札幌PHP勉強会 ナイトセッション #1 Kou
自⼰紹介 Kou (@kou_tech_1017) 札幌市内プログラマー スキル:PHP、Laravel、React、 Azure
この登壇における "翻訳" ある表現を、別の表現に変換すること PHP は実行までに何段階もの「翻訳」を重ねている
アジェンダ コードが動くまで 01 PHP 02 Opcache 03 JIT 04 PHP 8.4 とは とは 以降のJIT
コードが動くまで PHP
PHP コードが動くまで 1 2 3 ソースコード 字句・構文解析 コンパイル Zend VM ファイル トークン→AST AST→opcode opcode .php 4 5 実行 結果出力 を処理 HTML/JSON 毎リクエストごとにこの流れが繰り返される
PHP コードが動くまで 1 2 3 ソースコード 字句・構文解析 コンパイル Zend VM ファイル トークン→AST AST→opcode opcode .php 4 5 実行 結果出力 を処理 HTML/JSON
字句解析(Lexer) コードを意味のある最小単位(トークン)に分割する https://www.php.net/manual/ja/tokens.php
字句解析(Lexer) で出力。(※ T_OPEN_TAG と T_WHITESPACE は省略) token_get_all https://www.php.net/manual/ja/function.token-get-all.php <?php $a = 100; $b = 50; $price = $a + $b; T_VARIABLE → $a = → = T_LNUMBER → 100 ; → ; T_VARIABLE → $b = → = T_LNUMBER → 50 ; → ; T_VARIABLE → $price = → = T_VARIABLE → $a + → + T_VARIABLE → $b ; → ;
構文解析(Parser) トークンの並びから構造(AST)を組み立てる $price = $a + $b; の場合 代入(=) $price 加算(+) $a $b
AST とは (抽象構文木) Abstract Syntax Tree 「抽象」の意味 ソースコードから 構文上の記号を取り除き 意味だけを木構造にしたもの セミコロン、括弧、空白などは ASTには含まれない RFC: https://wiki.php.net/rfc/abstract_syntax_tree PHP 7 から導入 まではパーサーが 直接 を生成していた PHP 7でASTが間に入り パーサーとコンパイラが分離 PHP 5 opcode
AST 出力例 で確認 nikic/PHP-Parser <?php $a = 100; $b = 50; $price = $a + $b; 0: Stmt_Expression( Expr_Assign( var: $a, expr: 100 )) 1: Stmt_Expression( Expr_Assign( var: $b, expr: 50 )) 2: Stmt_Expression( expr: Expr_Assign( var: Expr_Variable(name: price) expr: Expr_BinaryOp_Plus( left: Expr_Variable(name: a) right: Expr_Variable(name: b) ) ) )
PHP コードが動くまで 1 2 3 ソースコード 字句・構文解析 コンパイル Zend VM ファイル トークン→AST AST→opcode opcode .php 4 5 実行 結果出力 を処理 HTML/JSON
コンパイル AST を opcode(バイトコード)に変換する (構文木) AST opcode 代入(=) $a ← 100 代入(=) $b ← 50 代入(=) ├── $price └── 加算(+) 0000 ASSIGN CV0($a) int(100) 0001 ASSIGN CV1($b) int(50) 0002 T0 = ADD CV0($a) CV1($b) 0003 ASSIGN CV2($price) T0 0004 RETURN int(1) ├── $a └── $b 命令1動作のシンプルな指⽰書 1
とは opcode Zend VM が理解できる命令セット — CPUにとっての機械語と同じ役割 opcode ADD ASSIGN ECHO RETURN 足し算 変数に値を入れる 出力する 値を返す やること すべてのPHPコードは最終的にこれらの組み合わせになる
PHP コードが動くまで 1 2 3 ソースコード 字句・構文解析 コンパイル Zend VM ファイル トークン→AST AST→opcode opcode .php 4 5 実行 結果出力 を処理 HTML/JSON
とは VM ソフトウェアで作られた仮想的なコンピュータ 本物のCPU Zend VM 機械語を読んで実行する opcode 0101 1010 1100 ... ADD VM = を読んで実行する ASSIGN ECHO ... 「opcodeを理解するために作られた、プログラムの中のコンピュータ」
は何をしているか Zend VM ディスパッチ ─ この4ステップをひたすら繰り返す ① 次のopcodeを取り出す ▼ ② 命令の種類を判別する ▼ ③ 対応するC関数を呼び出す ▼ ④ 処理を実行する まだ命令がある? → ①へ戻る 例:$price = 100 + 50; を取り出す 足し算だ ① ADD ② ③ ZEND_ADD_HANDLER() ④ 100+50 → ~0 ① ASSIGN ② 代入だ を取り出す ③ ZEND_ASSIGN_HANDLER() ④ ~0 → $price
ここまでの "翻訳" ソースコードがopcodeになるまでに3回の翻訳がある 1 字句解析(Lexer) ソースコード トークン 2 構文解析(Parser) トークン AST 3 コンパイル AST opcode ソースコード → PHPエンジンの言葉(opcode)に翻訳された
とは Opcache
がやっていること Opcache を共有メモリにキャッシュし、2回目以降の解析をスキップ opcode なし Opcache 毎回:字句解析 → 構文解析 → コンパイル → VM実行 → 出⼒ 同じファイルなのに、毎リクエスト最初から全部やり直す あり Opcache 初回:字句解析 → 構文解析 → コンパイル → [キャッシュ保存] → VM実行 → 出⼒ 2回目〜:[キャッシュ取得] → VM実行 → 出力 解析・コンパイルをスキップ! 共有メモリに保存 → 複数ワーカーで使い回せる
の "翻訳" への貢献 Opcache ❶ ソースコード → トークン SKIP ❷ トークン → AST SKIP ❸ AST → opcode SKIP は翻訳そのものを速くするのではなく、翻訳を省略する Opcache ただし、Zend VMのディスパッチは変わらない
Opcache の限界 が速くするのは 解析・コンパイル の省略だけ Opcache のディスパッチは何も変わっていない Zend VM ▼ 「Zend VMのディスパッチをもっと速くできないか?」 → JITの出発点
とは JIT
JIT が見ているもの ディスパッチの段取りは3つ。仕事をしているのは「実行」だけ ( なし) Zend VM JIT 取り出す 段 判別する 取 り C関数を呼ぶ 実行する あり JIT が直接実行 段取りなし CPU 「実行」だけが連続で⾛る 回繰り返し ← ×1000
ホットスポットの検出 最初はVMで実行し、よく通るコードだけをJIT対象にする 1 2 3 通常実行 カウント 閾値超過 のディスパッチで 普通に動かす VM 関数やループが 何回通ったか記録 一定回数を超えた → ホットスポット 4 コンパイル JIT そのコードだけ 機械語に変換 回しか通らないコードを変換しても、変換コスト分だけ損する 1
機械語への変換 を の機械語に変換する opcode CPU 機械語(x86-64) opcode ADD 100, 50 → ~0 ASSIGN $price ← ~0 がディスパッチを2回繰り返す VM mov eax, 100 add eax, 50 mov [price], eax JIT が直接実行 → 段取りなし CPU ディスパッチの段取りがなくなり、「実行」だけが連続で⾛る
JIT が加えた "翻訳" をさらに先へ — CPUの機械語へ翻訳する opcode 既存の翻訳(Opcacheでスキップ可能) ❶ ソースコード → トークン ❷ トークン → AST ❸ AST → opcode NEW 4 opcode 機械語 ホットスポットだけを対象に、VMを介さずCPUが直接実行できる形へ
以降のJIT PHP 8.4
〜 の の課題 PHP 8.0 8.3 JIT ごとのアセンブリ CPU 用、ARM用と ごとに低レベルコードが必要 x86 CPU 最適化の余地が少ない から直接機械語に変換 opcode 側で新CPU(RISC-Vなど)対応に アセンブリの専門知識が必要 「計算をまとめる」 「不要コードの除去」などの 高度な最適化を挟む場所がない → → JIT 対応コストが大きい 翻訳はするが質が上がらない
の場合(PHP 8.0〜8.3) DynASM のコード内にCPU別のアセンブリが直接書かれていた PHP JIT PHP JIT のコード(イメージ) の場合 の場合 // x86 // ARM mov eax, 100 add eax, 50 MOV R0, #100 ADD R0, R0, #50 // RISC-V ? → さらにもう1パターン必要... 最適化を1つ追加 → すべてのCPU向けアセンブリを書き直す必要がある
の場合( PHP 8.4〜) は 非依存の だけを生成。 別変換は フレームワークの仕事 IR PHP JIT CPU IR CPU IR のコード // CPU非依存のIRだけ書く PHP JIT ir_emit(LOAD, 100) ir_emit(ADD, 50) フレームワーク(PHP とは別プロジェクト) IR x86 mov eax, 100 add eax, 50 ARM MOV R0, #100 ADD R0, R0, #50 RISC-V li a0, 100 addi a0, a0, 50
: (中間表現)の導入 PHP 8.4 IR opcode → 機械語の間にIRという層が入った 〜 PHP 8.0 8.3 opcode 〜 機械語(CPU別) DynASM PHP 8.4 opcode NEW IR フレームワーク IR 機械語
つまり何が変わったのか 〜 PHP 8.0 8.3 密結合 〜 PHP 8.4 のコード内に 別アセンブリが直接存在 分離 は だけを生成 別変換は フレームワーク PHP JIT CPU PHP JIT IR CPU IR 最適化の追加 = 全CPU分書き直し 新CPU対応 = JIT全体に改修 最適化の追加 = IRだけ書けばいい 新CPU対応 = IRフレームワークだけ 機械語を生成すること⾃体は同じ。「CPU別の⾯倒を⾒る場所」がPHPの外に移った が レベルでCPU差を吸収していたのと同じ発想が、JITの中にも入った Zend VM opcode
が変えた 翻訳 の質 PHP 8.4 " " 翻訳の間にもう1段階(IR)を挟み、翻訳の質を上げた 〜 PHP 8.0 8.3 機械語 opcode 〜 PHP 8.4 opcode IR 最適化 機械語 NEW 翻訳回数が増えても、翻訳の質が上がれば最終的なコードは速くなる
PHP の "翻訳" まとめ が省略 Opcache ❶ ソースコード トークン Lexer ❷ トークン AST Parser ❸ AST opcode コンパイラ ❹ opcode 機械語 JIT は "翻訳" を重ねるたびに、CPUに近づいていく PHP
参考資料 字句解析(トークン一覧) 字句解析(token_get_all) AST導入 RFC(PHP 7) Opcache 公式ドキュメント Opcache 導入 RFC JIT 導入 RFC(PHP 8.0) JIT IR フレームワーク PR(PHP 8.4) opcode 定義(php-src) https://www.php.net/manual/ja/tokens.php https://www.php.net/manual/ja/function.token-get-all.php https://wiki.php.net/rfc/abstract_syntax_tree https://www.php.net/manual/en/book.opcache.php https://wiki.php.net/rfc/optimizerplus https://wiki.php.net/rfc/jit https://github.com/php/php-src/pull/12079 https://github.com/php/php-src/blob/master/Zend/zend_vm_opcodes.h