PHPカンファレンス2016 #phpcon2016 で PHPのASTについて話してきたこと

概要

PHP7 で導入された AST(Abstract Syntax Tree) について、その概要と、導入によるPHPの変化を解説しました。
おまけでASTの利用法についても少し。

AST の可視化は https://dooakitestapp.herokuapp.com/phpast/webapp/ にて試せます(動いてなかったらごめんなさい)。

動機

以前、まだphp7 がリリースされる前に闇PHP勉強会でASTについて発表したことがありました。
ASTの導入は、それ単体でのインパクトは小さく、php7の他の新機能に隠れがちではありますが、可能性という点においては他の機能に勝るとも劣らない仕組みです。

しかし、発表以来気になりつつもあまり追っていなかったのでした。
久しぶりにAST周りについて何か新しいことは起きてないかとググってみましたが、新しいことはおろか、ASTそのものについての話題すらほとんど見受けられないという状況でした。
この状況であれば、そろそろphp7リリースから1年経ちそうな現在であってもASTの話題はまだ古くはないかと思い、発表に至りました。
7.1でどう変化するのかも気になってましたし。

スライドについて

当初は、全AST Node について解説するつもりでいたのですが、全て解説したところであまり面白くないので、止めました。
スライドの中にはAST Node を解説するあたりに、全Node紹介をぶっこみました(が、最後の方はやる気無くなってるのが丸わかり)

また、いくつか講演時には利用しなかったスライドがありまして、これは30分に収める泣く泣く削ったものです。

トークについて

練習中はだいぶ早口でやってぎりぎり30分に収まるかなといった感じだったのですが、実際にやってみると思ったよりも早く進んでしまい、最後は時間が余って焦ってしまいました。
たぶん、練習中には話すつもりだった内容の一部が抜け落ちてる気がする。

最後に質問を受けたことについてですが、質問を受けたとき php-parser について(php-ast を利用して parse した結果を辿る別のライブラリと)勘違いしてまして、とんちんかんな答えをしてしまって申し訳ないです。
質問は、php のクラスを読み込み、(php-parser を使って) 分解、再構築することで (なにかしらの付加機能を加えた) クラス(コード)を作るようなライブラリを書いているが、(ASTを使って)もっと効率化することは出来ないかということでした。

今冷静になって答えると以下のようになります。
質問者の行っていることを、コンパイル時に行う(zend_ast_process を利用して AST を改変する) ことで多少パフォーマンスを改善することは出来ます。
しかしながら、おそらく質問者の書かれたライブラリでは毎回パースしているわけではないと思われるので、実行に与えるインパクトは小さいでしょう。

また、php-parser (内部では token_get_all を利用) を使ったほうが速いか、php-ast を使った方が速いか という話では、これはおそらく後者に分があるでしょう(ベンチマークを取ったわけではないので推測ですが)。
php-parse では 分解された token を php スクリプト構文木を作り上げているわけですが、実は token_get_all は 内部では字句解析と構文解析を行い、生成されたASTを捨てて、その過程のトークンだけを返しています。
これに対して、 php-ast は 字句解析と構文解析を行い、生成されたASTを php スクリプトから扱えるように変換しているだけです。

もちろん、php5 までは php-parser 以外に php スクリプトから構文木を扱う方法がなかったわけですからこれはこれで正しいのですが、ASTを構築するようになった php7 移行について言えば php-parser はだいぶ無駄なことをしていることになるのです。

補足

講演時には解説できなかったスライドや話題について、ここで補足しておきます。

Parse Error と Compiler Error

構文解析時に構文として受け入れられなかった場合は Parse Error で、これは php7 から例外となりました。つまり、回復可能(catch 可能) です。
ところが、Opcode 生成時に受け入れられなかったコードについては Compiler Error (Fatal) となります。

php スクリプトとして成り立ってないようなコードを eval した場合は 例外として catch 可能なのに、一見 php スクリプトっぽいのに NGなコードは Fatal です。
ちょっとどっかで嵌ることがありそうな気がしますね。

その他のコンパイル時最適化

最適化の例については半分ほど省略しました。以下に省略した最適化例を挙げます

静的関数展開(Opcode 変換)

http://www.slideshare.net/do_aki/php-ast#25

静的関数展開の一環で、関数呼び出しではなく Opcode に変換してしまいます。
以前の php であれば、これを実現するためには、関数ではなく言語構造にする(これらの関数を構文ルールに加えてしまう) しかなかったのではないかと思います。
しかしそれではさらに構文が増えることになり煩雑になるため、導入したくても出来なかったものではないのかなという憶測。

静的関数展開の無効化

http://www.slideshare.net/do_aki/php-ast#26

実は 静的関数展開 は無効にすることができる(ようなコードが用意されています)
どう使うのか、何の目的で用意されているのかは不明。

静的zval構築

http://www.slideshare.net/do_aki/php-ast#27

いままでの配列リテラルは実は結構コストかかっていたことを示す例となっています。
Opcache ではやっていそうな最適化ではありますが。

静的ショートサーキット

http://www.slideshare.net/do_aki/php-ast#28

これは、最適化と言うには少々微妙な例。
本来なら if (false) となったら、その内部のブロックを含め無くしてしまえば良いはずなのですが、そうはなっていません。
また、もともと実行される予定のないコードの部分について Opcode が生成されなくなるだけですので、実行速度への影響はほぼないでしょう。


コンパイルタイミングによって Opcode が変換する例

http://www.slideshare.net/do_aki/php-ast#31
http://www.slideshare.net/do_aki/php-ast#32

このスライドで説明したかったことは、autoload よりも あらかじめ include (require) しておくほうが最適化されるケースがあるよ ということです。
`php echo.php` は autoload を模しており、 autoload によってクラスが読み込まれる時に参照した定数は畳み込まれず、定数参照として Opcode が生成されます。

これがどの程度実際のパフォーマンスに影響してくるかは計測していませんが、もしかすると、Opcache は最初に作った Opcode をキャッシュするのではなく、2回目読み込んだ際に再度 Opcode を構築してキャッシュすることで速度向上が見込めるんじゃないか。という妄想も出来るわけですね。

HHVM における AST

http://www.slideshare.net/do_aki/php-ast#53

実は php 本家だけではなく、 hhvm についても少し調べていました。
hhvm は php5 用のパーサと php7 用のパーサを持っていて、(おそらくコンパイル時に)切り替えて使っているようですね。
本家の広い環境でコンパイルされることを前提としたコードとは異なり、 (C++ の)比較的新しい機能も利用しているところが特徴的だと感じました。

AST については、構成がほぼ同じなため、C で書く事と C++ で書く事の違いがはっきりと表れています。
hhvm の場合は1つのASTノード種を 1つの class で表現しています。

AST から phpスクリプトへの逆変換

トークでは php に実装された 関数を紹介しましたが、実は、php-ast-reverter (https://packagist.org/packages/tpunt/php-ast-reverter) というライブラリもあります。これは php-ast によって得た AST (のコピー) を元に php スクリプトを再構築するものです。
これを参考にすれば、AST から異なるコードへの変換は比較的容易ではないでしょうか。

最後に

phpカンファレンススタッフのみなさまありがとうございました。
また、他にもおもしろい講演がある中、だいぶニッチな箇所に焦点を当てた講演を聞いてくださった方ありがとうございます。

とりあえず、今後は今書いてる拡張をまともに動かせるようにしようとおもいます。