2016/12/11
第七回闇PHP勉強会
do_aki
1
updated 2016-12-13
このスライドは
• PHPカンファレンス2016で発表した
「PHP AST 徹底解説」 において説明し
きれなかった部分を補足した際に用いた
もの
• AST に関する部分については、元のスラ
イドにマージ済みなので
http://www.slideshare.net/do_aki/p
hp-ast を参照してください
@do_aki
@do_aki
http://do-aki.net/
PHPカンファレンス2016で
話したこと
PHP
Compiler in PHP
PHP Script
Opcode
Request
Output
Compiler
Lexing
Parsing
Compilation
VM
Execution
INCLUDE_OR_EVAL
php5 (1 pass / 151構文(5.6))
字句解析 + 構文解析 + Opcode生成
php7 (2 pass / 127構文(7.0))
字句解析+構文解析 Opcode生成
最適化の余地
PHP の
抽象構文木
<?php
1/(2+3);
種別
付属情報
子ノード
子ノード
token_get_all の話
token_get_all 関数
• PHP スクリプトを トークン分解して配列
にする関数
• nikic/PHP-Parser で利用されてる
• tokenizer 拡張 (デフォルトで有効)
7.0 からの変更
• 7.0 から第2引数に TOKEN_PARSE を指定で
きるようになった
– 指定なし: 字句解析のみ / 5.6 まで同様
– 指定あり: 構文解析もする / ast は破棄
• TOKEN_PARSE 指定でも Opcode 生成は省略
– zendparse を呼ぶが、
zend_compile_top_stmt は呼ばない
– Syntax Error (例外) は発生するがCompile
Error (Fatal) は発生しない
– const A = f(); のようなコードも受け入れる
7.0.12 での実行例
token_name(319) => T_STRING
コンパイル と
token_get_all の関係
字句解析 構文解析 Opcode生成
狭義のコンパイルAST を生成トークンに分解
従来からの
token_get_all
TOKEN_PARSE 付きの
token_get_all
文字列結合 Opcode の話
ソースコード(php スクリプト)
<?php
function hello ( $name ) {
echo “HELLO $name“ ;
}
hello ( “php“ ) ;
Opcode (vld)
line #* E I O op fetch ext return operands
----------------------------------------------------------------
2 0 E > EXT_NOP
1 RECV !0
3 2 EXT_STMT
3 NOP
4 FAST_CONCAT ~1 'Hello+', !0
5 ECHO ~1
4 6 EXT_STMT
7 > RETURN null
line #* E I O op fetch ext return operands
----------------------------------------------------------------
2 0 E > EXT_STMT
1 NOP
6 2 EXT_STMT
3 INIT_FCALL 'hello'
4 EXT_FCALL_BEGIN
5 SEND_VAL 'php'
6 DO_FCALL 0
7 EXT_FCALL_END
8 > RETURN 1
function
hello()
call
hello()
Opcode (vld without xdebug)
line #* E I O op fetch ext return operands
----------------------------------------------------------------
2 0 E > RECV !0
3 1 NOP
2 FAST_CONCAT ~1 'HELLO+', !0
3 ECHO ~1
4 4 > RETURN null
line #* E I O op fetch ext return operands
----------------------------------------------------------------
2 0 E > NOP
6 1 INIT_FCALL 'hello'
2 SEND_VAL 'php'
3 DO_FCALL 0
4 > RETURN 1
function
hello()
call
hello()
FAST_CONCAT
• encaps_list (変数を含む文字列) において、
要素が2つの時(変数の前後どちらかに文字列を加える時)
に FAST_CONCAT になる
(at zend_compile_encaps_list)
• 5.6 までは ADD_VAR + ADD_STRING
• ちなみに 3要素以上ならば ROPE_INIT,
ROPE_ADD, [ROPE_ADD,] ROPE_END
"{$a} lines"
5.6
7.0
#* E I O op fetch ext return operands
------------------------------------------------------
0 E > ADD_VAR ~0 !0
1 ADD_STRING ~0 ~0, '+lines'
#* E I O op fetch ext return operands
------------------------------------------------------
0 E > NOP
1 FAST_CONCAT ~1 !0, '+lines'
"{$a} / {$b} lines"
5.6
7.0
#* E I O op fetch ext return operands
------------------------------------------------------
0 E > ADD_VAR ~0 !0
1 ADD_STRING ~0 ~0, '+%2F+'
2 ADD_VAR ~0 ~0, !1
3 ADD_STRING ~0 ~0, '+lines'
#* E I O op fetch ext return operands
------------------------------------------------------
0 E > ROPE_INIT 4 ~3 !0
1 ROPE_ADD 1 ~3 ~3, '+%2F+'
2 ROPE_ADD 2 ~3 ~3, !1
3 ROPE_END 3 ~2 ~3, '+lines'
parse しつつ opcode 生成し
ていた時に、これを導入するの
は困難だったはず
AST により、容易に導入できる
ようになった例かなと
strlen 静的展開の話
静的関数展開(定数化)
• 関数呼び出しコストの削減
• 定数畳み込みとの組み合わせも有効
ex: strlen(A::HOGE) + 1 -> 5
strlen(’hoge’) -> 4
ord(’A’) -> 65 / 7.1~
chr(65) -> ‘A‘ / 7.1~
mbstring.func_overload
• strlen コールが mb_strlen に置き換わ
る
• EG(function_table) を操作して、上書
き
• コンパイル時に strlen が定数になって
しまうと機能しないのでは? という疑問
静的関数展開の無効化
• CG(compiler_options) に
ZEND_COMPILE_NO_BUILTINS ビットをセッ
トすることで静的関数展開を無効にできる
• CG(compiler_options) に
ZEND_COMPILE_NO_BUILTIN_STRLENビット
をセットすることで strlen の展開のみを
無効にできる
• 拡張ならば、 CG(compiler_options) を制
御可能
問題なかった
• mbstring 拡張の初期化(RINIT)時
func_overload が有効ならば
ZEND_COMPILE_NO_BUILTIN_STRLEN を
指定している
• func_overload は問題なく機能する
• func_overload が有効だと、 strlen
展開による恩恵を受けられない
同じコードから異なるOpcodeが
生成される話
定数の畳み込み
$sec_in_day = 60 * 60 * 24;
$sec_in_day = 86400;
※実は OpCache でも行われている
class A { const HOGE = ‘hoge‘; }
echo A::HOGE;
echo ‘hoge‘;
コンパイル時点で定義済みの定数に対してのみ有効
(autoload より pre include のほうが効きやすい)
コンパイルタイミングによって
Opcode が変化する例
class A { const X = 1; }
a.php
require_once 'a.php';
echo A::X;
echo.php
require_once 'a.php';
require_once 'echo.php';
require.php
> php echo.php
echo.php をコンパイルする時点
では a.php はコンパイルされて
いない
> php require.php
echo.php をコンパイルする時点
で a.php はコンパイル済み
line #* E I O op fetch ext return operands
-----------------------------------------------------------
2 0 E > INCLUDE_OR_EVAL 'a.php', REQUIRE_ONCE
3 1 FETCH_CONSTANT ~1 'A', 'X'
2 ECHO ~1
3 > RETURN 1
line #* E I O op fetch ext return operands
-----------------------------------------------------------
0 E > INCLUDE_OR_EVAL 'a.php', REQUIRE_ONCE
3 1 ECHO 1
2 > RETURN 1
> php echo.php
> php require.php (の時の echo.php)
HHVM の話
HHVM におけるコンパイル
• 2つのLexer (yylex)
Compiler5lex/Compiler7lex
• 2つのParser (yyparse)
HPHP::Compiler::Parser::parseImpl5
HPHP::Compiler::Parser::parseImpl7
• どちらも AST を生成する
HHVM における AST
• AST ノードの基底クラスである
HPHP::Construct があり、Statement
と Expression に分かれる
• HPHP::Compiler::Parser::parseImpl
が、parseImpl7 あるいは parseImpl5
を呼び出し、
HPHP::Compiler::Parser::m_tree に
StatementList が作られる
HPHP::Statement
• 構造を表すノードの
基本クラス
• HPHP::StatementList が
ZEND_STATEMENT_LIST に
相当
HPHP::Expression
• 評価式や値を表す
ノードの基本クラス
• AwaitExpression
あたりは hhvm なら
では
ast 操作拡張の話
php-ast
• https://github.com/nikic/php-ast
• astparse_file あるいは
astparse_code で AST 構築
• astNode をベースクラスとした astDecl
• リスト型のノード は Node に統合
• Zval型のノードは Node の exprプロパティ
• STMT_LIST(A) の子要素に STMT_LIST(B) が含
まれる場合は、B の子を A の子として併合
astkit
• https://github.com/sgolemon/astkit
• AstKit::parseString あるいは
AstKit::parseFile で AST構築
• AstKit をベースクラスとした AstKitList,
AstKitDecl, AstKitZval にマッピングさ
れる
• $AstKit->export でコードに変換
astparse_code('<?php 1 + 2;')
全ノードをphp スクリプトで扱える構造に変換 (CG(ast) は破棄)
C言語 (CG(ast))
array
astnode
kind: 520
flags: 1
lineno: 1
left: 1
right: 2
astNode
kind: 133
flags:0
lineno: 0
children:
AST_ZVAL
1
AST_ZVAL
2
AST_BINA
RY_OP +
AST_STMT
_LIST
ast_to_zval
php スクリプト (zval)
Astkit::parseString('1+2;')
先頭のノードのみ生成。操作により子の AstKit が生成される
C language (CG(ast) =
astkit_tree->tree)
AstKitList
AST_ZVAL
1
AST_ZVAL
2
AST_BINA
RY_OP +
AST_STMT
_LIST
php script (zval)
AstKit
AstKitZval
getChild(0) で生成
getChild(0,false) で生成
getChild(0) ならば int(1)
それぞれの特徴
• php-ast
– php スクリプトから扱いやすい
– 初期のコストが大きめ
– 異なるバージョンでの変換処理を拡張側で頑張っ
てる部分もある
• astkit
– C の ast そのままのメモリを操作
– 利用する箇所が部分的ならば低コストか
– ast 構造の変化によって php 側での操作が大き
く変わる
おしまい
(blank)

PHP AST 徹底解説(補遺)

Editor's Notes

  • #4 聞いて明日仕事に活かせるような話ではない