hnwの日記

PHP7はなぜ速いのか(zval編)

この記事はPHP Advent Calendar 2014の7日目です。


僕は先日のPHPカンファレンス2014でPHP7に関するLTをしました(参照:「PHPNGの動向」)。ただ、時間が限られていたこともあり、あまり踏み込んだ内容には触れませんでした。


本稿ではLTの内容から深掘りし、zvalという内部的な構造体がどう変わるのか、性能面のメリットにフォーカスして解説してみます。

PHPをとりまく状況

まず最近のPHPの話題をおさらいしておきましょう。


これまでPHPには実用レベルの別実装が存在しない状態が続いていましたが、HHVMの登場で状況が変わってきました。HHVMはFacebookが開発しているPHP実行環境で、PHPより高速で互換性も高いのが特徴です。Facebookで実際に利用されているだけでなく、他の企業でも商用サービスで利用する事例が増えてきています。


そんな中、2014年のはじめ頃にPHPの性能改善プロジェクトPHPNGがスタートしました。これはPHPコミッターでありZend Technologies社員でもあるDmitry Stogovによるもので、PHP内部のデータ構造の変更をすることでPHPの言語仕様を変えずに性能改善を行うものです。このプロジェクトは極めて順調に進み、2014年8月にはPHP7のベースプロジェクトとして採用されました。


説明が前後してしまいますが、PHPのバージョニングについても最近動きがありました。現時点でのPHPの最新版は5.6.3ですが、次のメジャーバージョンアップで7.0になることが決まっています(参照:「PHP RFC: Name of Next Release of PHP」)。PHP7は2015年11月のリリースを目指しており、2015年6月にはコードフリーズしてRC1が出る予定です(参照:「PHP RFC: PHP 7.0 timeline」)。


ちなみに、PHP7での性能改善はかなり進んでいます。下記グラフはZendCon 2014での発表からの引用ですが、WordPressベンチマークテストしたときにPHP5.6の約1.7倍、HHVMに匹敵する性能となっています。


(「PHPNG a New Core for PHP7」より)

PHP7でzvalが変わった

ここから本題です。既に紹介しましたが、PHP7では内部的なデータ構造が変更されており、これが性能改善に貢献しています。


中でも、zval構造体の変更はかなり大胆な内容だと言えます。zvalは全てのPHP変数を管理するためのデータ構造であり、今回の変更はPHPソースコード全体に影響するような大変更になっています。ここに注目したこと、これをやりきったことはかなりのhackだと思います。


以下、このzval構造体の変更内容について図を交えて説明していきます。

整数型の場合

以下の説明は全て64bit環境を前提にしています。次の図はPHP5の整数型の変数1個に対応するzvalを図示したものです。



この図の横幅は8bytesを表しています。これまでPHP5では「unused」となっている8+2=10bytesを無駄遣いしていたことがわかります。



こちらがPHP7での同様の図です。PHP5からPHP7ではzvalのサイズが24bytesから16bytesに減っていることがわかります。


また、PHP7では参照カウンタ(refcount)がzvalから消えているのも特徴的です。PHP5では全ての型がコピーオンライト方式で管理されているためzvalで参照カウンタを持っています。一方、PHP7ではNULL・真偽値・整数・浮動小数点数の4つの型では基本的にスカラー値がコピーされるようになり、参照カウンタが無くなっているのです。


さらに、PHP5の図に描かれているzvalへのポインタ(白い四角)がPHP7の方に無い点にも注意してください。PHP関数を呼び出すときの引数など、PHP5では多くの処理でzvalへのポインタを引き回していたのですが、PHP7ではzvalを引き回すようになっています。つまり、PHP5では整数型の変数1個を扱うときにzvalの実体と合わせて32bytes消費するのに対し、PHP7は同じ内容を16bytesで表現できるのです。また、ポインタ参照が1回減るという意味でもPHP7の方が有利なはずです。

文字列型の場合

次に、文字列型のときのzvalを見てみましょう。



PHP5の場合、zval構造体で文字列へのポインタと文字列長を管理しています。文字列本体はポインタの参照先に別途確保されています。



PHP7では文字列長と参照カウンタ(refcount)はzvalでなく、ポインタの先のzend_string構造体で管理するように変更されています。


zend_string構造体の先頭に型情報(typeとflags)がありますが、これは性能を稼ぐためにzvalと同じ情報を重複して持つような形になっています。さらに、hash_valueという計算したハッシュ値をキャッシュしておくための領域も新たに追加されています。これらの結果として消費メモリ量の観点ではPHP5に比べてPHP7の方が4bytes損をすることになります。


一方で、ポインタ参照が1回減っているのはPHP7の方が有利な点と言えます。


また、PHP5の文字列型では文字列長と文字列本体がメモリ上で離れており、その両者を読み書きするときにメモリアクセスやキャッシュアクセスが必ず2回必要になってしまいます。一方、PHP7の場合は両者が固まったメモリ領域に存在しています。同時に扱うデータはメモリ上での空間的局所性が高い方がキャッシュを有効に使えるわけですから、そうした観点でもPHP7の方が有利でしょう。

整数型の参照の場合

次に、参照代入したときのzvalを見てみましょう。これは、たとえば次のようなコードを書いたときに相当します。

<?php
$a = 1;
$b = &$a;


PHP5は、次のようにis_ref(参照かどうか)というフラグがzval中にあるため、通常の変数と参照との違いはこのフラグだけになります。



一方、PHP7ではis_refは廃止され、参照は型の一種として扱われるようになりました。次のように、typeの部分の値がIS_REFERENCEになります。



PHP5とPHP7とを見比べると、変数2つ分に対応するメモリ消費量は40bytesから56bytesに増加していることがわかります。


それだけでなく、通常の変数のときと構造が違いすぎるので、参照代入したタイミングでメモリ確保とzvalのコピーのコストもかかってしまいます(「整数型の変数(PHP7)」と「整数型の参照(PHP7)」を見比べてみてください)。これはPHP5のときに比べて参照代入のコストが高くなっていると言えるでしょう。

まとめ

特徴的な型について図を交えて紹介してきましたが、PHP7でのzvalの変更点はおおよそ次のようにまとめられます。

  • 参照以外の全てのPHP型について、データの持ち方と引き回し方の工夫でポインタ参照を1段減らした
    • 参照だけは冗長な構造になった
  • スカラー値に対応する変数型については参照カウントを利用したコピーオンライトを廃止し、値のコピーを行うようになった
  • 他の型についてはPHP5と挙動は同じまま、データ構造の変更でメモリアクセスの局所性を高めた


そもそも論として、コピーオンライトは大きいサイズのデータコピーを避けるような場合に有利な戦略ですから、これまでのPHPで整数のようなサイズの小さい変数値までコピーオンライトの対象にしていたのが失敗だったとも言えます。また、ポインタ参照を減らしたり、メモリアクセスの局所性を高めるなども発想として突飛だとは思いません。ただ、こうした「当たり前」の改善をPHPのような巨大プロジェクトでやり切るのは凄いことだと思います。


ちなみに、PHP7ではzval以外に文字列と配列についてもデータ構造の見直しが行われています。こうした細かいチューニングの積み重ねがPHP7の速さの要因と言えるでしょう。詳しくは参考資料をご確認ください。

参考資料