y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

OPcache の最適化器の今 (PHP カンファレンス 2017 発表資料)

10/8 (日) に開催された PHP カンファレンス 2017 で「OPcache の最適化器の今」という内容で発表しました。発表資料を公開します。

www.slideshare.net

PHP カンファレンスという大きなイベントでもあり、発表のレベル感をどうしようかと悩みましたが*1、結果的にはかなり初心者向けに振った構成にしたつもりです。その分、もしかすると事前知識があった方には物足りない発表になってしまったかもしれません。本記事では、実際にソースコードを詳しく追ってみたいという方のために、どこに何が書かれているのかを簡単に補足します。

OPcache の最適化の実装は、ext/opcache/Optimizer ディレクトリにあります。zend_optimizer.c に定義されている zend_optimize_script 関数が起点になります*2PHP ファイルごとにこの関数が実行されます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/zend_optimizer.c#L1192

PHP 7.1 以降での最適化の処理は、コールグラフ構築の手前までと、コールグラフ構築以降で大きく二段階に分かれています。前半の処理は、PHP ファイル内に定義されている関数、メソッドごとに zend_optimize_op_array, zend_optimize が呼び出され、zend_optimize 関数で最適化の各 pass が実行されます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/zend_optimizer.c#L945

前半の pass で面白いのは pass 5 の制御フローグラフ構築だと思います。この処理は block_pass.c の zend_optimize_cfg 関数に実装されています。ここで、制御フローグラフの構築 (zend_build_cfg), 基本ブロック内の最適化 (zend_optimize_block), ジャンプ命令の最適化 (zend_jmp_optimization), 到達しない基本ブロックの除去 (zend_cfg_remark_reachable_block) といった処理が行われます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/block_pass.c#L1878

コールグラフの構築はファイル内の関数全体を解析することになるので、各関数の最適化が終了した後に zend_optimize_script 関数から呼び出されます。実装は zend_call_graph.c の zend_build_call_graph 関数にあります。関数の呼び出し関係を解析してコールグラフを構築 (zend_analyze_calls) した後、再帰呼び出しによるサイクルの発見 (zend_analyze_recursion) も行われます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/zend_call_graph.c#L245

ちなみに、構築したコールグラフをトポロジカルソートする処理 (zend_sort_op_arrays) が用意されているのですが、これは未実装で、次のような TODO コメントが記載されています。ここに限らず、最適化器の実装を見ていると、このような TODO コメントがところどころに見られます。決して完成した実装ではなく、開発チームが改善のための努力を続けているという雰囲気が感じられます。

static void zend_sort_op_arrays(zend_call_graph *call_graph)
{
	(void) call_graph;

	// TODO: perform topological sort of cyclic call graph
}

PHP 7.1 の最適化器の目玉だったデータフロー解析は、dfa_pass.c の zend_dfa_analyze_op_array 関数と zend_dfa_optimize_op_array 関数に実装されています。前者がデータフロー解析処理、後者がその結果を利用した最適化処理です。静的単一代入形式への変換、その上での解析などは zend_dfa_analyze_op_array 関数から実行されます。一方、定数伝播 (pass 8) や不要コード除去 (pass 14) の最適化は zend_dfa_optimize_op_array 関数から実行されます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/dfa_pass.c#L42
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/dfa_pass.c#L540

なお、データフロー解析に関しては、最適化オプションの設定によって二通りの実行経路があります。通常は、zend_optimize_script 関数内で、コールグラフを構築した後に zend_dfa_analyze_op_array 関数、zend_dfa_optimize_op_array 関数が順に呼び出されます。一方、最適化オプションを変更して pass 7 を無効にするとコールグラフ構築が行われず、その場合は zend_optimize 関数から pass 6 として zend_optimize_dfa 関数が呼ばれます。zend_optimize_dfa 関数の内部で zend_dfa_analyze_op_array 関数と zend_dfa_optimize_op_array 関数が呼び出されるので、結果的には同じようにデータフロー解析が行われます。

このようにして、OPcache の内部で最適化の各 pass が順に実行されていきます。最後に PHP 7.1 での高速化のポイントであった命令ハンドラの選択についてですが、これは全ての最適化が終わった後に、以下の箇所から zend_redo_pass_two_ex が呼び出されて実行されます。
https://github.com/php/php-src/blob/php-7.2.0RC3/ext/opcache/Optimizer/zend_optimizer.c#L1300

zend_redo_pass_two_ex では、各命令に対して zend_vm_set_opcode_handler_ex 関数を呼び出します。この関数は OPcache ではなく Zend/zend_vm_execute.h に定義されています。zend_vm_execute.h は各バイトコード命令のハンドラ関数が実装されているファイルで、全体で 6 万行以上あります*3。このファイルを見てみると、大量の命令ハンドラの中から効率的なものが選択されている様子が分かるかもしれません。

*1:一般的には、大イベントになるほど初心者が多くなる傾向があるだろうと思いますが、さまざまなセッションが並行して行われる中で私の発表を聴きにきてくれる方というのは、相当マニアックな方だけかもしれない・・・という難しさがありました。

*2:これ以降のソースコードへのリンクは、現時点での最新版である PHP 7.2.0 RC3 を対象とします。

*3:これは手書きではなく、スクリプトを通して生成されるファイルです。Zend/README.ZEND_VM ファイルに説明があります。