「PHP と SAPI と ZendEngine3 」についてを #phperkaigi で話してきたこと

概要

各種 PHP の実行環境 と PHP 内部の動きについて、 PHPerKaigi2018 前夜祭 で話してきました。

経緯とか

PHPerKaigi2018 の存在を知ったのが 1/9

https://twitter.com/do_aki/status/950678439429775360


締め切りギリギリでCfPを送り、

https://twitter.com/do_aki/status/952703045342674944


モンハン買うべきか迷いつつ (結局買ったし狩った)

https://twitter.com/do_aki/status/955751331804823554

資料作りに苦しみながらも

https://twitter.com/do_aki/status/966979682200530944
https://twitter.com/do_aki/status/968118433337372672


それでも、なんとか発表までこぎつけました。


当初は、以前 相模原で開催された PHP 勉強会で発表した 「PHP と SAPI と ZendEngine2 と」 (https://www.slideshare.net/do_aki/php-and-sapi-and-zendengine2-and) の内容を今ならもっと詳しく話せるかな という感じで気軽に考えていたのですが、
4年たってだいぶ忘れてる部分もあるし、当時よりも(中途半端に)詳しく知ってしまったためにかえって説明しづらい機能があったりして、なかなか大変でした。

トークについて

PHP実行環境 というのは、実際に使っている環境については知っていても、それ以外の環境のことは案外知らなかったりするもので、個々の環境についてではなく、全体像を知ってもらうことを意識しました。

また、内部(Zend Engine) については、当初はソースコードを元に各機能を解説していくつもりでしたが、思ったよりも広域すぎたので諦め、 "PHP Script から見える部分" を中心に話しました。

正しさ と わかりやすさ を両立させたつもりでしたが、うまく伝わったでしょうか。



ちなみに、 uzulla さんが 急遽行ったLT で話したことに対して、自分のトーク内容が関連していたりしたのは本当に偶然でした。

https://twitter.com/do_aki/status/972032383858831360


トーク後の質疑応答で適切な回答ができなかったことが悔やまれるので今後の課題。


PHPerKaigi2018 について

本編 では そーだいさんの話を聞いて、質疑応答のうまさにすごいなーと思いつつ、

https://twitter.com/do_aki/status/972289820197208064

あとはほとんどずっと TrackB の Interactive Round Table で PhpStorm や開発環境について話してました。
居心地よくて長い間占拠してしまってすみません。でも楽しかった。


懇親会で、何名かの方に(前日の)トーク良かったと言っていただけたのがとてもうれしかったです。
郡山さんのバイタリティに圧倒されたのも良い思い出。

最後に

主催の長谷川さん、スタッフの皆さんありがとうございました。
来年も開催されることを期待しています。


次は福岡行きたい

同じように見えて異なる PHP の文字列についての話

はじめに

本記事は PHP Advent Calendar 2017 18日目 です。

先に断っておきます。
この記事の内容は、 php スクリプトを書く上で全く必要のない知識です。

知ってすぐ何かに役立つような情報を求めていたらごめんなさい。
https://qiita.com/advent-calendar/2017/php にはもっとたくさんの素晴く役立つ記事がありますのでどーぞ。

なお、検証に利用した php は 7.2.0 です。

PHP の文字列

PHP の文字列は、どれも同じ "文字列" ではあるのですが、中身がちょっと違うとことがあるのをご存知でしょうか。


例えば、

<?php
$str1 = 'A';
$str2 = sprintf('%s', 'A');

`$str1` と `$str2` はどちらも同じ `'A'` という文字列になりますが、違いがあります。
(変数名が違うとか、異なる zval である とかいう意味ではありません。)

<?php
$str3 = 'class';
$str4 = 'hoge';

`$str3` と `$str4` は、異なる文字列ですが、同じ性質を持っています。
そして同じ性質を持っているにも関わらず、少しだけ違うところがあります。

php スクリプト側には表れない、メモリの確保の仕方が異なるのです。

インターン化文字列

php スクリプト上に現れるすべての文字列リテラルインターン化されます。

インターン化、つまり同じ箇所に割り当てられたメモリを共有しているわけです。

<?php
function hoge() {
	$hoge = 'hoge';
	$fuga = 'hoge';
}

`$hoge`, `$fuga` は別の変数ですが、同じメモリに配置された `hoge` を参照していますし、
さらには 関数名、 変数名 の `hoge` も同じメモリ上に配置されます。(関数名や変数名に直接アクセスすることはあまりないかと思いますが、 `call_user_func` や `$$` で使われます)

この仕組みは 5.4 から導入された *1 もので、詳しくは @hnw さんの記事 (http://d.hatena.ne.jp/hnw/20151205) がわかりやすいので参照してください。

特殊なインターン化文字列

さてこの文字列のインターン化についてですが、実は php スクリプトに現れなくてもインターン化されている文字列が存在します。

1. 空文字 *2
2. 1byte 文字列(0-255) *3
3. その他、ZendEngineにおける頻出文字列 (KNOWN_STRING) *4

空文字がインターン化されていることで、 `if ($str === '')` みたいな条件式があっても、比較するたびに `''` がインスタンス化されるわけではなく、すでにインスタンス化された文字列が利用されることで高速化につながるというわけです。


1byte 文字列については 7.0 から新たに導入されたもの *5 です。
`'A'` のような直接的な表現だけでなく、 `substr` や `explode` を呼び出した結果が 1byte になる場合も、この文字列が利用されます。


KNOWN_STRING は 7.1 から導入されたもの *6 で、これについては、 php スクリプトの実行よりも、 ZendEngine 内部の最適化の意味合いが強そうです。
C言語でも、至る所に文字列リテラルが記述されていると、その分フットプリントが大きくなってしまいます。これを共通化することで、メモリ削減と参照の局所化につなげているのでしょう。


これらは、他のインターン化文字列とは異なり、 php 起動時に ZendEngine が常に生成しています。
そして、最も大きな違いは `emalloc` 使わずに直接 `malloc` によってメモリが割り当てられているということです。

つまり、 `memory_get_usage` の戻り値に全く含まれない部分なんですね。

`memory_get_usage` は php が利用しているメモリ量を返しますが、この値は `emalloc` という ZendEngine のメモリマネージャを介して割り当てられたメモリのみが集計対象となっています。
`malloc` によって割り当てられたメモリについては `memory_get_usage` の引数を true にしても取得できません。

冒頭コードの説明

というわけで、最初にお見せしたコードは

`$str1`
インターン化された 1byte 文字列
`$str2`
通常の文字列 (非インターン化文字列)
`$str3`
インターン化された KNOWN_STRING
`$str4`
インターン化された文字列


というように、それぞれ メモリ確保のされ方が異なる文字列なのでした。 (`$str1` と `$str3` は同じ様式ですが)

php を書く上でこの違いを意識することはないですし、意識しようとしても php スクリプトの世界でこれらが違うということを知る手段はありません。

では、どうやってこの違いがわかったのかというと、php 本体のソースコードを読んだうえで、 innerval という 雑な拡張を書いて試したからです。

<?php
$str1 = 'A';
$str2 = sprintf('%s', 'A');
$str3 = 'class';
$str4 = 'hoge';

check('$str1', $str1);
check('$str2', $str2);
check('$str3', $str3);
check('$str4', $str4);

function check($name, $str) {

	$state = [];
	$state[] = is_interned_string($str) ? 'INTERNED' : null;
	$state[] = is_persistent_string($str) ? 'PERSISTENT': null;
	$state[] = is_permanent_string($str) ? 'PERMANENET': null;

	echo "{$name} \"{$str}\" is ". (implode(",", array_filter($state)) ?: 'normal string'),"\n";
}

https://github.com/do-aki/innerval/コンパイルして有効にしてから 上記のような簡単なスクリプトを実行してみると、

$str1 "A" is INTERNED,PERSISTENT,PERMANENET
$str2 "A" is normal string
$str3 "class" is INTERNED,PERSISTENT,PERMANENET
$str4 "hoge" is INTERNED

という結果が得られます。*7

INTERNED はそのまま インターン化された という意味で、 PERSISTENT が malloc によって確保されたメモリであることを表しています。

PERMANENET は、リクエストをまたいで有効なメモリ (リクエスト終了時に破棄されない) を意味し、例えば opcache によって割り当てられた共有メモリを参照している場合に、 非 PERSISTENT な PERMANENET 文字列になります。 (opcache が絡むと話がややこしくなるので詳しくは割愛)

7.2 での変化

さて、このように表舞台にはでてこないインターン化文字列ですが、 7.2 になってとても大きな変更がありました。

Thread Safe版が再サポートされたのです。
https://github.com/php/php-src/commit/c6982995504b6e21e8a5ade29cfb16a55196dc43#diff-b27dcc67dedd7af10b0fb1ec4fd540dc

実は、5.4 で導入された時点では Thread Safe版 PHP でもインターン化されていました。
ところが、7.0 の開発過程で Thread Safe版 PHP におけるインターン化は無効化されていました https://github.com/php/php-src/commit/f4cfaf36e23ca47da3e352e1c60909104c059647#diff-b27dcc67dedd7af10b0fb1ec4fd540dc (すごいコミットコメントだ)

7.2 になって、それがまた復活したというわけです。

Thread Safe版 PHP を使っている人(いるの?) には朗報ですね。

まとめ

こんなもん知って何に役立つの? って思われるかと思いますが、何の役にも立ちません。
冒頭で免責した通りです。

ただ、オモテには現れないたくさんの改善の積み重ねが 今の PHP を作っているんだなぁと。

(そして、 Thread Safe版 PHP の不憫さよ。)

再考:列挙型

はじめに

php で列挙型と言えば、 @hiraku さんの http://qiita.com/Hiraku/items/71e385b56dcaa37629fe みたいな実装がほとんどかと思います。

自分もだいたいこんな感じでいいかなと思っているのですが、2点ほどどうしても実現したいことがありました。

1. IDE で補完したい
2. 同じ定数値であっても異なる列挙型ならば別物としたい


これについて、第117回 PHP勉強会@東京 でLT したスライドがこちらです。

下記は、話した内容をもう少し詳しく書き起こしたものになります。

1. IDE で補完したい

そのままの話で、 __callStatic を利用した場合、補完できません。
補完できないということは、IDE のコード支援機能(例えばリファクタリング)が利用できないということです。

これについては、DocComment を入れれば済む話ではあります。

例えば、ゲームの難易度を表す列挙型があったとして、こんな感じにクラスコメントを追加することで対応可能です。

<?php
/**
 * @method static GameDifficulty HARD()
 * @method static GameDifficulty NORMAL()
 * @method static GameDifficulty EASY()
 */
final class GameDifficulty extends Enum
{
    const HARD = 'hard';
    const NORMAL = 'normal';
    const EASY = 'easy';
}

2. 同じ定数値であっても異なる列挙型ならば別物としたい

例えば、チーズの種別でハードを表すのに CheeseType::HARD としたとして、これはゲーム難易度のハード GameDifficulty::HARD とは別物です。
ところが、現状の列挙型実装だと

<?php
$hard = CheeseType::HARD();

var_dump($hard->valueOf() === GameDifficulty::HARD); // bool(true)

みたいなことが起きてしまいかねません。

では、下記のような比較用のメソッドを Enum クラスに生やしてはどうでしょう。

<?php
public function is(Enum $lhs)
{
    return get_class($this) === ($lhs)
        && $this->value() === $lhs->value();
}

これでも先の例よりはましです。
ただ、下記のようにミスした時に、(実行時ではなく)記述した時に気づけたほうが嬉しいですね。

<?php
$hard = CheeseType::HARD();

var_dump($hard->is(GameDifficulty::HARD())); // bool(false)

trait を利用した 列挙型

そんなわけで、以前、trait を使って getter や setter を IDE で補完できるようにしたことがあった *1 ので、その仕組みを利用して列挙型を実装してみました。

以下のように利用します。

同じようなことを2度(const と constant as)書かないとならないのが少々面倒ではありますが、クラスコメントで補完が効くようにする手間とそう変わらないのでいいかなと。

(後から気づいたけど、 コンストラクタを private にするなら、 コンストラクタでのチェックは不要かも)

EnumTrait の特徴

use … as でメソッドを再定義することで、IDE からそれぞれのメソッドが存在すると認識されます (少なくとも PhpStorm では認識されます)

trait を利用することで、`self` タイプヒンティング(型宣言)が、use されたクラスを表すことになるため、`is` メソッドが他のクラスを受け付けません。

また、シングルトンにすることで、オブジェクト自身の比較が定数値の比較と同じことになり、厳密な比較が可能になります

constant は、 `debug_backtrace` を使っているので、一見遅そうですが、 一度呼ばれた後はキャッシュ (static 変数) が効くため、呼び出し回数によっては `__callStatic` を呼ぶよりも早かったりします。

PhpStorm のバグ

ただ、残念ながら問題がありました。

PhpStorm では、 定数を生成するメソッドの利用箇所を参照する機能が利用できないのです。

バグレぽ出すも、なかなか修正されず。 


仕方ないので、 CommentDoc も併用しています。


また、

<?php
var_dump($difficulty->is(CheeseType::HARD())); // throw TypeError`

のコードは、静的にエラー出せるはずなのですが、
今の PhpStorm では trait 上の self は trait 自身 (つまり EnumTrait) と認識してしまうため、警告されません。

今後の進化に期待したいところ。

(あれ? ということは、結局現状の列挙型と大して変わらなくない?)

まとめ

従来の列挙型の欠点を trait を利用することで解消する試みを紹介しました。

残念ながら 今の PhpStorm による静的解析では、trait を適切に解釈しないためちょっとイマイチな部分もあります。

闇PHP勉強会 #yamiphp でASTとsignalについて話してきたこと

時間が開いてしまいましたが、先週 第七回闇PHP勉強化に参加し、トークしてきました。

タイトルは「PHP AST 徹底解説(補遺)」および、「signal の話 或いは Zend Signals とは何か」です。

前者は、過日 PHPカンファレンス2016 で発表した「PHP AST 徹底解説」の補足集で、PHPカンファレンス2016 では時間の都合上省略したり、発表後に指摘された内容について調べたものになります。

後者は PHP における signal 処理についての概要と、7.1 における変化を解説しました。

PHP AST 徹底解説(補遺) の、ASTに関する部分については 「PHP AST 徹底解説」にマージしてありますので、興味があればこちらを参照してください。


signal については、調べていくと OS の挙動を確認する必要があったりして、なかなか複雑だったりします。
また、signal は max_execution_time の実装においても利用されているのですが、時間の計測についてもなかなか奥深いものがあって、詳しく説明すると長くなりすぎると思ったので、あくまで signal にかかわる部分のみを解説しました。

そのため、想定よりもトーク時間が短くなってしまいました。(時間足りない詐欺してすみません。)

どっかで調べたことをもう一度整理してまとめたいところ。


自分以外のトークについて

「realpathキャッシュとOPcacheの面倒すぎる関係」

realpath キャッシュの実装が(phpのほかの機構を利用せず、)独自に実装されてるあたりが面白いなと。

ただこのキャッシュの実装。衝突時に単方向リンクドリスト*1となる固定長(1024)のハッシュマップ*2なので、コリジョンが多く発生するとパフォーマンスがだいぶ落ちそう。
単純に考えても、1024 file 以上なら必ずコリジョンは発生しますし、ハッシュキーの生成法も固定*3なので、もしかするとコリジョンが発生しやすいファイルパスもあるのではないかと思いました。

php スクリプトは、利用するテンプレートエンジンによっては大量に生成されますし、ある程度の規模であれば、realpathキャッシュを切るというのは正しい戦略なのかもしれません。

JITのコードを読んでみた

JIT についてはほかの言語での実装についても全く知らず、 php での実装の仕方を知ることができたのは大きかったです。
LuaJIT のコード生成エンジンを利用しているとは。

最適化という点で考えると、やはり(ネイティブな)型を意識していく方向に向かうのは当然ではありますが、今のところの実装は良くも悪くも php らしさがあるなと思いました。

このPHP拡張がすごい!2017

php は、他のLLと比べて拡張が作られることが少ないと感じていましたが、それでもこれだけの拡張が書かれていたのだと圧倒されました。

ゃ、もっとみんなカジュアルに拡張書けばいいのに。

XPathソースコード検索

ASTを XML に変換すれば、構造の一部を XPath で抽出できるという発想は斬新だなぁと。
聞き手からは XPathより JQuery セレクタ という声も聞こえましたけど、XPath、強力なんですよね。

jQuery セレクタCSSセレクタをベースにしていて、あまり柔軟な指定って出来なくて、長期的なメンテナンス性を考えなくて良いこういう時の XPath はほんと強力。

ストリームフィルタで暗号化しよう!

7.1 でmcrypt が非推奨になって、OpenSSL 使え (http://php.net/manual/ja/migration71.deprecated.php) とあるけど、OpenSSL だと暗号/複合処理が全体をまとめてでしかできないよね、と。

その時は、ブロック単位での暗号化インタフェースを (php拡張作って)実装すれば、それを利用してユーザ定義ストリームフィルタを登録してやればいいんじゃないかななんて言ってたけど、よくよく考えれば、openssl_encrypt をつかっても、IV を更新していくことでブロック単位で暗号化可能なんじゃないかなー?(未検証)

最後に

主催していただいた @hnw さん、会場提供の pixiv さん、ありがとうございました。

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

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