43
PHP AST 徹徹徹徹 ( 徹徹 ) 2016/12/11 第第第第 PHP 第第第 do_aki 1 updated 2016-12-13

PHP AST 徹底解説(補遺)

  • Upload
    doaki

  • View
    1.359

  • Download
    0

Embed Size (px)

Citation preview

Page 1: PHP AST 徹底解説(補遺)

PHP AST 徹底解説( 補遺 )

2016/12/11 第七回闇 PHP 勉強会

do_aki1

updated 2016-12-13

Page 2: PHP AST 徹底解説(補遺)

このスライドは

• PHP カンファレンス 2016 で発表した 「 PHP AST 徹底解説」 において説明しきれなかった部分を補足した際に用いたもの

• AST に関する部分については、元のスライドにマージ済みなので http://www.slideshare.net/do_aki/php-ast を参照してください

Page 3: PHP AST 徹底解説(補遺)

@do_aki

@do_aki

http://do-aki.net/

Page 4: PHP AST 徹底解説(補遺)

PHP カンファレンス 2016 で話したこと

Page 5: PHP AST 徹底解説(補遺)

PHP

Compiler in PHP

PHP Script

Opcode

Request

Output

Compiler

Lexing

Parsing

Compilation

VM

Execution

INCLUDE_OR_EVAL

Page 6: PHP AST 徹底解説(補遺)

php5 (1 pass / 151 構文 (5.6))

字句解析 + 構文解析 + Opcode 生成

php7 (2 pass / 127 構文 (7.0))

字句解析 + 構文解析 Opcode 生成

複雑

最適化の余地

Page 7: PHP AST 徹底解説(補遺)

PHP の 抽象構文木

<?php1/(2+3);

種別

付属情報

子ノード

子ノード

Page 8: PHP AST 徹底解説(補遺)

あじぇんだ

1 token_get_all の話

2 文字列結合 Opcode の話

3 strlen 静的展開 の話

4 同じコードから異なる Opcode の話

5 HHVM の話

6 ast 操作拡張 の話

Page 9: PHP AST 徹底解説(補遺)

token_get_all の話

Page 10: PHP AST 徹底解説(補遺)

token_get_all 関数

• PHP スクリプトを トークン分解して配列にする関数

• nikic/PHP-Parser で利用されてる

• tokenizer 拡張 (デフォルトで有効)

Page 11: PHP AST 徹底解説(補遺)

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(); のようなコードも受け入れる

Page 12: PHP AST 徹底解説(補遺)

7.0.12 での実行例

token_name(319) => T_STRING

Page 13: PHP AST 徹底解説(補遺)

コンパイル と token_get_all の関係

字句解析 構文解析 Opcode 生成

狭義のコンパイルAST を生成トークンに分解

従来からのtoken_get_all

TOKEN_PARSE 付きの token_get_all

Page 14: PHP AST 徹底解説(補遺)

文字列結合 Opcode の話

Page 15: PHP AST 徹底解説(補遺)

ソースコード (php スクリプト )

<?php

function hello ( $name ) {

echo “HELLO $name“ ;

}

hello ( “php“ ) ;

Page 16: PHP AST 徹底解説(補遺)

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()

Page 17: PHP AST 徹底解説(補遺)

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()

Page 18: PHP AST 徹底解説(補遺)

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

Page 19: PHP AST 徹底解説(補遺)

"{$a} lines"5.6

7.0

#* E I O op fetch ext return operands------------------------------------------------------0 E > ADD_VAR ~0 !01 ADD_STRING ~0 ~0, '+lines'

#* E I O op fetch ext return operands------------------------------------------------------0 E > NOP1 FAST_CONCAT ~1 !0, '+lines'

Page 20: PHP AST 徹底解説(補遺)

"{$a} / {$b} lines"5.6

7.0

#* E I O op fetch ext return operands------------------------------------------------------0 E > ADD_VAR ~0 !01 ADD_STRING ~0 ~0, '+%2F+'2 ADD_VAR ~0 ~0, !13 ADD_STRING ~0 ~0, '+lines'

#* E I O op fetch ext return operands------------------------------------------------------0 E > ROPE_INIT 4 ~3 !01 ROPE_ADD 1 ~3 ~3, '+%2F+'2 ROPE_ADD 2 ~3 ~3, !13 ROPE_END 3 ~2 ~3, '+lines'

Page 21: PHP AST 徹底解説(補遺)

parse しつつ  opcode 生成していた時に、これを導入す

るのは困難だったはず

AST により、容易に導入できるようになった例かなと

Page 22: PHP AST 徹底解説(補遺)

strlen 静的展開の話

Page 23: PHP AST 徹底解説(補遺)

静的関数展開 ( 定数化 )

• 関数呼び出しコストの削減• 定数畳み込みとの組み合わせも有効ex: strlen(A::HOGE) + 1 -> 5

strlen(’hoge’) -> 4

ord(’A’) -> 65 / 7.1 ~

chr(65) -> ‘A‘ / 7.1 ~

Page 24: PHP AST 徹底解説(補遺)

mbstring.func_overload• strlen コールが mb_strlen に置き

換わる• EG(function_table) を操作して、上

書き

• コンパイル時に strlen が定数になってしまうと機能しないのでは? という疑問

Page 25: PHP AST 徹底解説(補遺)

静的関数展開の無効化• CG(compiler_options) に ZEND_COMPILE_NO_BUILTINS ビットをセットすることで静的関数展開を無効にできる

• CG(compiler_options) に ZEND_COMPILE_NO_BUILTIN_STRLENビットをセットすることで strlen の展開のみを無効にできる

• 拡張ならば、 CG(compiler_options) を制御可能

Page 26: PHP AST 徹底解説(補遺)

問題なかった

• mbstring 拡張の初期化 (RINIT) 時 func_overload が有効ならば ZEND_COMPILE_NO_BUILTIN_STRLEN を指定している

• func_overload は問題なく機能する

• func_overload が有効だと、 strlen 展開による恩恵を受けられない

Page 27: PHP AST 徹底解説(補遺)

同じコードから異なる Opcodeが生成される話

Page 28: PHP AST 徹底解説(補遺)

定数の畳み込み

$sec_in_day = 60 * 60 * 24;

$sec_in_day = 86400;※ 実は OpCache でも行われている

class A { const HOGE = ‘hoge‘; }

echo A::HOGE;

echo ‘hoge‘;

コンパイル時点で定義済みの定数に対してのみ有効( autoload より pre include のほうが効きやすい)

Page 29: PHP AST 徹底解説(補遺)

コンパイルタイミングによって 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.phpecho.php をコンパイルする時点では a.php はコンパイルされていない

> php require.phpecho.php をコンパイルする時点で a.php はコンパイル済み

Page 30: PHP AST 徹底解説(補遺)

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)

Page 31: PHP AST 徹底解説(補遺)

HHVM の話

Page 32: PHP AST 徹底解説(補遺)

HHVM におけるコンパイル

• 2 つの Lexer (yylex)Compiler5lex/Compiler7lex

• 2 つの Parser (yyparse)HPHP::Compiler::Parser::parseImpl5HPHP::Compiler::Parser::parseImpl7

• どちらも AST を生成する

Page 33: PHP AST 徹底解説(補遺)

HHVM における AST• AST ノードの基底クラスであるHPHP::Construct があり、 Statement と Expression に分かれる

• HPHP::Compiler::Parser::parseImpl が、 parseImpl7 あるいは parseImpl5 を呼び出し、 HPHP::Compiler::Parser::m_tree に StatementList が作られる

Page 34: PHP AST 徹底解説(補遺)

HPHP::Statement

• 構造を表すノードの基本クラス

• HPHP::StatementList が ZEND_STATEMENT_LIST に相当

Page 35: PHP AST 徹底解説(補遺)

HPHP::Expression

• 評価式や値を表すノードの基本クラス

• AwaitExpression あたりは hhvm ならでは

Page 36: PHP AST 徹底解説(補遺)

ast 操作拡張の話

Page 37: PHP AST 徹底解説(補遺)

php-ast• https://github.com/nikic/php-ast

• ast\parse_file あるいは ast\parse_code で AST 構築

• ast\Node をベースクラスとした ast\Decl• リスト型のノード は Node に統合• Zval型のノードは Node の exprプロパティ

• STMT_LIST(A) の子要素に STMT_LIST(B) が含まれる場合は、 B の子を A の子として併合

Page 38: PHP AST 徹底解説(補遺)

astkit• https://github.com/sgolemon/astkit

• AstKit::parseString あるいは AstKit::parseFile で AST構築

• AstKit をベースクラスとした AstKitList, AstKitDecl, AstKitZval にマッピングされる

• $AstKit->export でコードに変換

Page 39: PHP AST 徹底解説(補遺)

ast\parse_code('<?php 1 + 2;')

全ノードをphp スクリプトで扱える構造に変換 (CG(ast) は破棄)

C 言語 (CG(ast))

array

ast\node kind: 520 flags: 1 lineno: 1 left: 1 right: 2

ast\Node kind: 133 flags:0 lineno: 0 children:

AST_ZVAL1

AST_ZVAL2

AST_BINARY_OP +

AST_STMT_LIST

ast_to_zval

php スクリプト (zval)

Page 40: PHP AST 徹底解説(補遺)

Astkit::parseString('1+2;')

先頭のノードのみ生成。操作により子の AstKit が生成される

C language (CG(ast) = astkit_tree->tree)

AstKitList

AST_ZVAL1

AST_ZVAL2

AST_BINARY_OP +

AST_STMT_LIST

php script (zval)

AstKit

AstKitZval

getChild(0) で生成

getChild(0,false) で生成getChild(0) ならば int(1)

Page 41: PHP AST 徹底解説(補遺)

それぞれの特徴

• php-ast– php スクリプトから扱いやすい– 初期のコストが大きめ– 異なるバージョンでの変換処理を拡張側で頑張っ

てる部分もある• astkit– C の ast そのままのメモリを操作– 利用する箇所が部分的ならば低コストか– ast 構造の変化によって php 側での操作が大

きく変わる

Page 42: PHP AST 徹底解説(補遺)

おしまい

Page 43: PHP AST 徹底解説(補遺)

(blank)