hnwの日記

PHPのインターン化文字列とは何か

このエントリは闇PHP Advent Calendar 2015の3日目です。なぜか@do_akiさんによる4日目の記事「ZEND_TICKS と tick 関数」を読んだ後で書いています。


本稿では、あまり日本語での説明を見たことがないPHPインターン化文字列(interned strings)について紹介します。

文字列のインターン化とは

文字列のインターン化というのは多くのプログラミング言語で採用されているテクニックで、同じ不変文字列がプログラム中に何度も登場するような場合に、毎回文字列をコピーするのではなく同じ文字列を共有することで実行時間やメモリ消費量などを有利にするようなものです。Wikipediaの「String interning」なども参照してください。


internというのは軍などで使われる単語で、抑留というのが日本語で一番近い単語だと思いますが、どうもピンとこないので「interned=インターン化」で統一することにします。


PHPでは、PHP 5.4以降でプログラム中の文字列リテラルやその他の固定文字列がインターン化されるようになり、性能向上に貢献しています。文字列のインターン化は暗黙に行われるため、通常プログラマが意識することはありません。

PHPインターン化文字列とメモリイメージ

インターン化文字列の例を見ていきましょう。たとえば、下記プログラムのふるまいがPHP 5.3と5.4で変わっています。

<?php
$a = "foo";
$b = "foo";


$a$bは別の変数ですが、同じ値の文字列リテラルが代入されている、という状況です。


PHP 5.3では、プログラム中の2つの"foo"は別のメモリ領域にそれぞれ確保されていました。



これが、PHP 5.4からは同じ静的文字列が繰り返しコピーされることはなくなり、同じメモリ領域を利用するようになりました。



この例だけだと大した性能改善には見えないかもしれませんが、現実のプログラムではこれが「ちりも積もれば」で効いてくるということです。

インターン化されるのは文字列リテラルだけではない

上の例では文字列リテラルインターン化されているのを見ましたが、それ以外にもプログラム中の固定文字列は全てインターン化されます。つまり、クラス名・関数名・変数名なども全てインターン化の対象です。また、組み込みのクラス名・関数名も全てインターン化されています。


逆に言うと、例えばローカル変数名を$var_dumpにすると、$fooなどの名前にするよりも僅かにCPUとメモリが節約できるというわけです。実用性は全くありませんが、豆知識としては面白いですよね。

PHP 5.4とAPCの死

このインターン化文字列の導入についてはPHP 5.4のリリースノートにも明示的には書かれていません。PHP 5.4リリース当時はtraitなどの機能追加の方が話題になっており、多くの人にとって内部実装に大変更があったという認識は無かったと思います。しかし、この変更はPHP本体の実装を複雑にしただけでなく、コードキャッシュ用の拡張モジュールであるAPCにも大きな影響を与えました。


PHP 5.3時代のAPCはそれなりに安定していた印象でしたが、PHP 5.4への対応をうたったAPC 3.1.10以降はbetaリリースのままで、安定版がリリースされることはありませんでした。また、高負荷サイトをPHP 5.4+APCで運用しているとキャッシュ周りのトラブルが起きやすい印象がありました。印象論ですが、APCインターン化文字列に絡むバグを最後まで取り切れなかったんじゃないでしょうか。身近なプロジェクトではAPCに見切りをつけてPHP5.4+OPcacheに切り替えたりしていましたね。


PHP 5.5でOPcacheがPHP本体に同梱されたことでAPCの死が確定的になったという見方もできますが、その前から死に体だったというのが個人的な印象です。OPcacheのOSS化がなかったらPHPの勢いは今より弱まっていたかもしれません。

OPcacheでのインターン化文字列の取り扱い

一方、OPcacheはインターン化文字列に特化した性能改善を行っています。


OPcacheなしのPHPではインターン化文字列の実体はプロセスごとに別になります。また、PHPではリクエストごとに全てのリソースを破棄しますので、インターン化文字列もリクエストのたびにリセットされてしまいます。つまり、インターン化文字列の導入により同一リクエスト内での文字列コピーは削減できているものの、リクエスト間・プロセス間で見ると同じ文字列をコピーしており、まだムダがあると言えます。


これに対し、OPcacheではインターン化文字列プールを共有メモリ上に置き、プロセスをまたいで利用するようになっています。全リクエストで同じ文字列を共有してコピー回数を減らせるわけですから、システム全体で見ると実行時間やメモリ消費量の観点で有利になります。


ちなみに共有メモリ上のインターン化文字列プールのサイズはopcache.interned_strings_bufferディレクティブで指定できます。デフォルトでは4MBですので、不足しているようなら増やしておくのが良いでしょう。容量オーバーしてもプロセスごとのインターン化文字列プールにフォールバックして書きこまれるだけなので、小さい値を設定していると即座に危険というわけではありません。念のため。