A remarkable improvement on data structure in PHP7 PHP7の内部実装から学ぶ
 性能改善テクニック hnw Developers Summit 2015 KANSAI (2015/9/4)発表資料
自己紹介 ❖ @hnw ❖ 勤務先:KLab株式会社 ❖ カレーとバグが大好物 ❖ PHP歴15年 ❖ PHPや周辺エクステンションにバグレポ・PR多数
アジェンダ ❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
まずはアンケート
普段PHPを 書いている方?
実際にPHP7を
 試したことがある方?
❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
PHP7? ❖ PHP 5.6.xの次のバージョンがPHP 7.0.0 ❖ PHP6はスキップ ❖ 約10年ぶりのメジャーバージョンアップ
さいきんのPHP7 ❖ 予定通りalphaバージョン、betaバージョンをリリース ❖ 現在最新はPHP 7.0.0 RC1 ❖ 11月に正式版リリース予定
PHP7、どれくらい変わる? ❖ PHP7で採択されたRFCは48個 ❖ 参考:PHP 5.6は17個、PHP 5.5は20個 →普段のマイナーバージョンアップよりは変化が大きい
PHPのRFCシステム ❖ 新機能の導入にはRFCと呼ばれる説明ページが必要 ❖ 提案内容や変更による影響などを書く ❖ MLで議論後、投票によって採用・不採用が決まる ❖ 過半数または2/3以上の同意が必要(内容による) ❖ スピード感は無いが、十分機能している印象 ❖ 議論を追うのに非常に便利
PHP7で変わらないこと ❖ PHPは言語仕様の変更に対して非常に保守的 ❖ PHP7でも後方互換性は重視されている ❖ 移行コストは十分低いはず
❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
致命的エラーが例外になった ❖ PHP5までの致命的エラー ❖ エラーハンドリングできず、即座に終了していた ❖ PHP7の致命的エラー ❖ Errorという新しい例外になった ❖ トップレベルに到達すると今まで通りのエラーになる →ユニットテストで致命的エラーから復帰できる
??演算子の新設 ❖ nullでなければその値を、nullなら右オペランドを返す ❖ isset()と同様に未定義値に対しても使える ❖ ようやくisset()地獄から解放されるぞ!
無名クラスの導入 ❖ クラス定義と同時にインスタンス化できる構文を導入 ❖ その場限りのインスタンスを作りたいときに便利
AST(抽象構文木)の導入 ❖ 解釈フェーズが
 1段増えた ❖ 難しい文法が導入
 できるようになった ❖ opcodeの最適化を 行うようになった Zend VM opcode Parser Lexer token PHP Zend VM opcode Parser Lexer token PHP Opcode Compiler AST PHP 5 PHP 7
返り値のタイプヒントをサポート ❖ 関数の返り値に型が指定できるようになった ❖ 抽象クラスやインターフェースで指定すると便利
スカラ型のタイプヒントをサポート ❖ 以下の型が引数・返り値で指定できるようになった ❖ int型 ❖ float型 ❖ string型 ❖ bool型 ❖ 議論が続いていたが、ついに決着
非推奨だった機能を廃止 ❖ PHP5.6までに非推奨になった機能をPHP7で廃止 ❖ ereg関数(preg関数使ってね) ❖ mysql関数(mysqli関数かPDO使ってね) ❖ その他
アジェンダ ❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
PHP7は速いらしい ❖ 「PHP5より倍速い」 ❖ 「HHVMとほぼ互角」 ❖ ホントに?
PHP7の性能(1) Zeevのブログ記事(2014/7)より
PHP7の性能(2) DmitryのZendCon 2014での発表(2014/10)より
PHP7の性能(3) RasmusのFluent 2015での発表(2015/4)より
PHP7の性能(4) ❖ 性能改善を積み重ねてきたことがわかる
速くなりすぎ? ❖ PHP7は確かに速い ❖ 約1年間で2倍の高速化(WordPressで比較) ❖ 今までも性能改善をサボっていたわけじゃない ❖ 過去10年間(PHP5.0→5.6)で2倍の高速化 ❖ 5.4以降は頭打ちの感さえあった
何があったのか? ❖ メジャーバージョンアップならではの大手術 ❖ 基本的なデータ構造の変更 ❖ 高速化チームの裁量が大きかった ❖ RFC「Move the phpng branch into master」 ❖ 様々なアイデアを約1年間に渡って実現
❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
性能改善に「銀の弾丸」はない ❖ 性能改善:ボトルネックを順につぶしていく作業 ❖ 一発で改善、みたいなことは滅多にない ❖ ボトルネック以外の箇所をいくら改善してもムダ ❖ ボトルネックを見つけるところからスタート
PHP7チームが見つけたボトルネック ❖ PHP5でベンチマークテストしてみた ❖ メモリアロケーション・ハッシュ操作の時間 30% ❖ 内部関数の実行時間 30% ❖ VMの実行時間 30% ❖ その他 10%
PHP7チームが見つけたボトルネック ❖ PHP5でベンチマークテストしてみた ❖ メモリアロケーション・ハッシュ操作の時間 30% ❖ 内部関数の実行時間 30% ❖ VMの実行時間 30% ❖ その他 10% この辺は今までチューニングしてきた
PHP7チームが見つけたボトルネック ❖ PHP5でベンチマークテストしてみた ❖ メモリアロケーション・ハッシュ操作の時間 30% ❖ 内部関数の実行時間 30% ❖ VMの実行時間 30% ❖ その他 10% ほぼ手つかず、改善の余地ありそう
メモリ周りがボトルネック候補 ❖ 具体的に何をすればいいのか? ❖ 普段のチューニングとは違う知識が必要
Q. 何がボトルネック? ❖ N次元行列の行列積の実行時間(C言語で記述)
行列積(擬似コード)
Q. 何がボトルネック? ❖ 急に5倍ほど遅くなっている ?
答: L3キャッシュミス ❖ 行列全体がキャッシュに乗り切らなくなった →CPUコアは余裕があるのにメモリで待たされる ❖ 行列積の計算では同じ要素のreadがN回発生 ❖ 2回目以降キャッシュヒットするかどうかの差
キャッシュの階層構造 L3 cache L1 cache L2 cache CPU core memory L1 cache L2 cache CPU core ❖ 一般に、上位キャッシュほど速くて小さい
メモリの遅さ ❖ CPU 1クロックサイクル:0.3ns ❖ L1キャッシュヒット:1.2ns ❖ L2キャッシュヒット:3ns ❖ L3キャッシュヒット:12ns ❖ メモリアクセス:60-100ns 雑に言うと、メモリアクセス1回の間に演算300回できる
メモリの遅さ ❖ メモリアクセスはボトルネックになりやすい ❖ CPUに対して相対的に遅い ❖ キャッシュを有効に使えるかどうかが重要 ❖ 上位キャッシュのヒット率アップ→性能改善 ❖ 例:データ構造のムダを削る
キャッシュライン ❖ キャッシュは「キャッシュライン」単位で行われる ❖ キャッシュラインサイズ:64bytesなど ❖ 連続したメモリ領域が一度にキャッシュされる ❖ 同時に使うデータは近いメモリ領域に置くと有利
メモリ関連の最適化 ❖ キャッシュヒット率を向上 ❖ 可能な範囲でデータサイズを削る ❖ キャッシュラインを意識 ❖ 同時に使うデータは近いメモリ領域に配置する PHP7でも、このような最適化が行われている
❖ さいきんのPHP7 ❖ PHP7の新機能 ❖ PHP7って速いの? ❖ どこがボトルネック? ❖ PHP7のデータ構造
データ構造の変更 ❖ PHP7では性能改善のためデータ構造を見直した ❖ PHPの変数(zval) ❖ 文字列(zend_string) ❖ 配列(HashTable / Bucket)
PHP5のzval(int型) ❖ 未使用領域が多い
 (intの場合24bytes中10bytes) ❖ 必ずポインタ参照 ❖ 参照カウンタを持つ、コピーオンライト
PHP7のzval(int型) ❖ 計16bytes ❖ 参照カウントしない、代入では常にコピー
PHP7のzvalへの評価 ❖ メモリ上のムダな隙間を減らす変更 ❖ ポインタ参照が1段減った ❖ int型・float型・bool型でコピーオンライト廃止 →サイズ削減によりキャッシュヒット率向上
PHP5のzval(string型) ❖ 文字列長と文字列本体が別の領域になっている pointer refcount type unusedis_ref unused pointer to zval string length string characters zval
PHP7のzval(string型) ❖ 文字列長と文字列本体を連続領域に配置 ❖ 参照カウントあり、同じ文字列は使い回す string characters flags pointer type reservedflags refcount type gc_info hash_value string length zend_string zval
PHP7の文字列への評価 ❖ 文字列長と文字列本体を連続領域に配置 →キャッシュラインを意識した変更
ねんがんの配列をてにいれたぞ! ❖ PHP7から、「本物の配列」が導入されました ! 「いままで配列なかったの?」 「はい」
配列とは ❖ (典型的には)連続するメモリ領域を確保する ❖ インデックスは0から連続する数字 ❖ 読み書きが高速 リンゴ バナナ トマト ニンジン 0 1 2 3
連想配列とは ❖ 文字列をキーにできる ❖ そこそこ読み書きも速い(配列よりは遅い) "apple" 0 "banana" "carrot" 1 2 3 4 5 6 7 キーのハッシュ値を計算 "apple"=>リンゴ"banana"=>バナナ "tomato"=>トマト "tomato" "carrot"=>ニンジン
PHP5の配列 ❖ PHP5までは連想配列しか無かった ❖ $array[1]でも連想配列アクセスしていた ❖ 配列より複雑な構造、キャッシュに優しくない
PHP5の配列 nTableSize nNextFreeElement HashTable nTableMask nNumOfElements pListTail arBuckets unused pInternalPointer pListHead pDestructor nApp lyCo unt persi stent bAppl yProte ction unused h nKeyLength unused pData pDataPtr pListNext pListLast pNext pLast arKey Bucket zval pointer to Bucket (Bucket *)[] pointer to Bucket pointer to Bucket pointer to Bucket Another(Bucket*) whichisinthesamehashbin
PHP7の配列 ❖ PHP7から配列と連想配列が別の構造になった ❖ 配列のときはハッシュテーブルが省略される ❖ 内部で自動的に選択される ❖ 配列・連想配列のデータは連続領域に置かれる
PHP7の配列 arData flagsrefcount type gc_info zend_array u nTableMask nNumUsed nNumOfElements nTableSize nInternalPointer nNextFreeElement pDestructor zval Bucket[] h (hash value) key (キーへのポインタ) h key 0 1 h key 2 zval zval ❖ 他言語の配列と同じ構造
 (数字キーのみの場合)
PHP7の配列への評価 ❖ 配列と連想配列の区別ができた ❖ 配列のときのムダが減った ❖ データが連続領域に置かれるようになった ❖ 1要素あたりデータサイズ減少(72bytes→32bytes) →キャッシュヒット率・キャッシュラインを意識した変更
PHP7の新データ構造 ❖ 3つともキャッシュを意識したデータ構造 ❖ 性能面でインパクトの大きい変更 ❖ 「PHPなかなかやるじゃん」
PHP7の新データ構造 ❖ 3つともキャッシュを意識したデータ構造 ❖ 性能面でインパクトの大きい変更 ❖ 「PHPなかなかやるじゃん」 ❖ 他の言語では以前から採用されている ❖ Python、Ruby、JavaScriptエンジン
性能改善の面白さ ❖ 隣接領域の知識が役立つことがある ❖ 言語処理系ならCPUやメモリの知識 ❖ Webアプリならミドルウェア・ネットワークの知識
まとめ ❖ PHP7は高速、PHP5.6の倍程度の性能 ❖ オシャレな新機能を採用 ❖ 現代のCPUに合わせたデータ構造変更を行った ❖ データサイズ削減(キャッシュヒット率向上) ❖ 連続領域にデータを配置(キャッシュラインを意識)
ご静聴 ありがとう ございました

PHP7の内部実装から学ぶ性能改善テクニック