OPcache の最適化器の今
内山 雄司 (@y__uti)
Japan PHP Conference 2017
自己紹介
内山 雄司 (@y__uti)
◦ http://y-uti.hatenablog.jp/ (phpusers-ja)
仕事
◦ 受託開発の会社 (株式会社ピコラボ) でプログラマをしています
興味
◦ プログラミング言語処理系
◦ 機械学習
2017-10-08 JAPAN PHP CONFERENCE 2017 2
ベンチマークテスト
0.0
0.5
1.0
1.5
2.0
2.5
3.0
5.5.38 5.6.31 7.0.24 7.1.10 7.2.0RC3
実行時間[秒]
PHP バージョン
Zend/bench.php の実行時間 (各 5 回の実行の平均)
OPcache 無効 OPcache 有効
2017-10-08 JAPAN PHP CONFERENCE 2017 3
Core i5-3337U
CentOS 7 (VM 上の Guest OS)
ベンチマークテスト
0.0
0.5
1.0
1.5
2.0
2.5
3.0
5.5.38 5.6.31 7.0.24 7.1.10 7.2.0RC3
実行時間[秒]
PHP バージョン
Zend/bench.php の実行時間 (各 5 回の実行の平均)
OPcache 無効 OPcache 有効
2017-10-08 JAPAN PHP CONFERENCE 2017 4
Core i5-3337U
CentOS 7 (VM 上の Guest OS)
PHP 7 で高速化
ベンチマークテスト
0.0
0.5
1.0
1.5
2.0
2.5
3.0
5.5.38 5.6.31 7.0.24 7.1.10 7.2.0RC3
実行時間[秒]
PHP バージョン
Zend/bench.php の実行時間 (各 5 回の実行の平均)
OPcache 無効 OPcache 有効
2017-10-08 JAPAN PHP CONFERENCE 2017 5
Core i5-3337U
CentOS 7 (VM 上の Guest OS)
OPcache 有効時
PHP 7.1 以降も速度向上
発表の流れ
ベンチマーク結果の紹介
PHP のプログラム実行と OPcache
OPcache の最適化器の変遷
◦ PHP 5.5 ~ 7.0
◦ PHP 7.1
◦ PHP 7.2
2017-10-08 JAPAN PHP CONFERENCE 2017 6
PHP プログラムの実行方式
バイトコードインタプリタ方式
2017-10-08 JAPAN PHP CONFERENCE 2017 7
PHP ファイルをコンパイルする
インタプリタで実行する
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 8
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 9
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 10
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 11
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 12
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 13
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 14
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
プログラム実行の様子
プログラムをバイトコード命令列にコンパイルして逐次実行
2017-10-08 JAPAN PHP CONFERENCE 2017 15
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
OPcache の役割
バイトコード命令をキャッシュして再利用
2017-10-08 JAPAN PHP CONFERENCE 2017 16
バイトコードをキャッシュする
PHP ファイルをコンパイルする
キャッシュから
バイトコードを取得する
キャッシュ済み?
インタプリタで実行する
NO YES
OPcache の役割
バイトコード命令を最適化する
2017-10-08 JAPAN PHP CONFERENCE 2017 17
キャッシュから
バイトコードを取得する
キャッシュ済み?
インタプリタで実行する
NO YES
バイトコードを最適化する
バイトコードをキャッシュする
PHP ファイルをコンパイルする
発表の流れ
ベンチマーク結果の紹介
PHP のプログラム実行と OPcache
OPcache の最適化器の変遷
◦ PHP 5.5 ~ 7.0
◦ PHP 7.1
◦ PHP 7.2
2017-10-08 JAPAN PHP CONFERENCE 2017 18
PHP 5.5 の最適化器
2017-10-08 JAPAN PHP CONFERENCE 2017 19
PASS 3
連続するジャンプの短絡など
PASS 10
NOP 命令の除去
PASS 9
一時変数のメモリ領域共有
PASS 5
制御フローグラフを用いた最適化
PASS 2
条件分岐の置き換えなど
PASS 1
定数式の置き換えなど
素朴な最適化の例
出発点:最適化を行わない場合
2017-10-08 JAPAN PHP CONFERENCE 2017 20
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 1 定数式の置き換え
1 + 2 > 0 は true だと分かるので・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 21
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 1 定数式の置き換え
コンパイル時に計算してしまう
2017-10-08 JAPAN PHP CONFERENCE 2017 22
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 JMPZ <bool>, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 2 条件分岐の置き換え
if 文の true/false はコンパイル時に決まっているので・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 23
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 JMPZ <bool>, ->8
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 2 条件分岐の置き換え
NOP (飛ばない場合) か JMP (飛ぶ場合) に置き換える
2017-10-08 JAPAN PHP CONFERENCE 2017 24
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 NOP
3 JMP ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 3 連続ジャンプの短絡
JMP 命令の飛び先も JMP 命令なので・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 25
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 NOP
3 JMP ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 3 連続ジャンプの短絡
最後の飛び先まで一気にジャンプする
2017-10-08 JAPAN PHP CONFERENCE 2017 26
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 NOP
3 JMP ->6
4 ECHO 'A'
5 JMP ->9
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 10 NOP 命令の除去
NOP (何もしない命令) を実行しても何もしないのだから・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 27
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 NOP
1 NOP
2 NOP
3 JMP ->6
4 ECHO 'A'
5 JMP ->9
6 ECHO 'B'
7 JMP ->9
8 ECHO 'C'
9 RETURN null
 ソースコード  コンパイル結果
PASS 10 NOP 命令の除去
消す
2017-10-08 JAPAN PHP CONFERENCE 2017 28
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 JMP ->3
1 ECHO 'A'
2 JMP ->6
3 ECHO 'B'
4 JMP ->6
5 ECHO 'C'
6 RETURN null
 ソースコード  コンパイル結果
PASS 5 制御フローグラフ
処理の流れをグラフ構造で表現して各種最適化を行う
2017-10-08 JAPAN PHP CONFERENCE 2017 29
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
 ソースコード  制御フローグラフ
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
9 RETURN null
8 ECHO 'C'
PASS 5 制御フローグラフ
この例ではどこを通るか決まっているので・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 30
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
0 ADD ~0 1, 2
1 IS_SMALLER ~1 0, ~0
2 JMPZ ~1, ->8
 ソースコード  制御フローグラフ
3 JMPZ <bool>, ->6
4 ECHO 'A'
5 JMP ->7
6 ECHO 'B'
7 JMP ->9
9 RETURN null
8 ECHO 'C'
PASS 5 制御フローグラフ
ここまで最適化される (PHP 5.5 の頃からこの程度はできていた)
2017-10-08 JAPAN PHP CONFERENCE 2017 31
function test()
{
if (1 + 2 > 0) {
if (false) {
echo "A";
} else {
echo "B";
}
} else {
echo "C";
}
}
# op return operands
----------------------------------
0 ECHO 'B'
1 RETURN null
 ソースコード  コンパイル結果
PHP 5.6 の最適化器
2017-10-08 JAPAN PHP CONFERENCE 2017 32
PASS 4
関数呼び出しの効率化
PASS 11
未使用リテラルの除去
PASS 9
PASS 5PASS 1
PASS 2
PASS 3 PASS 10
PHP 7.0 の最適化器
2017-10-08 JAPAN PHP CONFERENCE 2017 33
PASS 9
PASS 5PASS 1
PASS 2
PASS 3 PASS 10
PASS 4 PASS 11
PASS 12
関数スタックフレームの最適化
PHP 7.1 の最適化器
2017-10-08 JAPAN PHP CONFERENCE 2017 34
PASS 9
PASS 1
PASS 2
PASS 3 PASS 10
PASS 11
PASS 5
PASS 6
データフロー解析に基づく最適化
PASS 12
PASS 7
コールグラフの構築
PASS 4 (関数呼び出しの効率化)
→ PASS 16
関数のインライン化 (極めて限定的)
PASS 7 コールグラフ
関数間の呼び出し関係をグラフ構造で表現して各種最適化を行う
2017-10-08 JAPAN PHP CONFERENCE 2017 35
function test() {
return f();
}
function f() {
return x() + y();
}
function x() { ... }
function y() { ... }
 ソースコード  コールグラフ
f
test
x y
ただし現時点では解析結果を積極的には利用していない様子
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 36
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  バイトコード命令列 (データフロー解析前)
0 ASSIGN !0, 3
1 ADD ~2 !0, 3
2 ASSIGN !1, ~2
3 IS_SMALLER ~2 0, !1
4 JMPZ ~2, ->7
5 ASSIGN !0, 4
6 JMP ->9
7 MUL ~2 !0, 5.6
8 ASSIGN !1, ~2
9 ADD ~2, !0, !1
10 RETURN ~2
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 37
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
9 ADD ~2, !0, !1
10 RETURN ~2
0 ASSIGN !0, 3
1 ADD ~2 !0, 3
2 ASSIGN !1, ~2
3 IS_SMALLER ~2 0, !1
4 JMPZ ~2, ->7
5 ASSIGN !0, 4
6 JMP ->9
7 MUL ~2 !0, 5.6
8 ASSIGN !1, ~2
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 38
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
return $a + $b;
$a = 3;
$b = $a + 3;
if ($b > 0)
$a = 4; $b = $a * 5.6;
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 39
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
return $a + $b;
$a = 3;
$b = $a + 3;
if ($b > 0)
$a = 4; $b = $a * 5.6;
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 40
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
return $a + $b;
$a = 3;
$b = $a + 3;
if ($b > 0)
$a = 4; $b = $a * 5.6;
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 41
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
$a <- $a or $a;
return $a + $b;
$a = 3;
$b = $a + 3;
if ($b > 0)
$a = 4; $b = $a * 5.6;
静的単一代入形式 (SSA) と呼ばれる
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 42
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
$a <- $a or $a;
$b <- $b or $b;
return $a + $b;
$a = 3;
$b = $a + 3;
if ($b > 0)
$a = 4; $b = $a * 5.6;
PASS 6 データフロー解析
プログラムの各点でデータが取り得る型や値を解析する
2017-10-08 JAPAN PHP CONFERENCE 2017 43
function test()
{
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
 ソースコード  制御フローグラフ
$a <- $a or $a; // [3, 4]: int
$b <- $b or $b; // ?: [int, float]
return $a + $b;
$a = 3; // 3: int
$b = $a + 3; // 6: int
if ($b > 0)
// 4: int
$a = 4;
// ?: float
$b = $a * 5.6;
解析結果の用途
型や値に応じた効率的なバイトコード命令への変換
2017-10-08 JAPAN PHP CONFERENCE 2017 44
function test():int
{
$a = 1;
$a = $a + 1;
return $a;
}
# op return operands
----------------------------------
0 ASSIGN !0, 1
1 ASSIGN_ADD !0, 1
2 VERIFY_RETURN_TYPE !0
3 RETURN !0
 ソースコード  コンパイル結果 (データフロー解析なし)
0 QM_ASSIGN !0 1
1 PRE_INC !0
2 RETURN !0
 コンパイル結果 (データフロー解析あり)
PHP 7.1 時点ではそれほど積極的な実装はされていない様子
解析結果の用途
型に特化したハンドラの選択
"ADD 命令" は大量にある (データの種類や型によって "適切な ADD" を選択)
2017-10-08 JAPAN PHP CONFERENCE 2017 45
ZEND_ADD_SPEC_CONST_CONST_HANDLER
ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER
ZEND_ADD_SPEC_CONST_CV_HANDLER
ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER
ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER
ZEND_ADD_SPEC_TMPVAR_CV_HANDLER
ZEND_ADD_SPEC_CV_CONST_HANDLER
ZEND_ADD_SPEC_CV_TMPVAR_HANDLER
ZEND_ADD_SPEC_CV_CV_HANDLER
ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV_HANDLER
ZEND_ADD_LONG_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV_HANDLER
ZEND_ADD_DOUBLE_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV_HANDLER
7.1 で追加
解析結果の用途
型に特化したハンドラの選択
"ADD 命令" は大量にある (データの種類や型によって "適切な ADD" を選択)
2017-10-08 JAPAN PHP CONFERENCE 2017 46
ZEND_ADD_SPEC_CONST_CONST_HANDLER
ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER
ZEND_ADD_SPEC_CONST_CV_HANDLER
ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER
ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER
ZEND_ADD_SPEC_TMPVAR_CV_HANDLER
ZEND_ADD_SPEC_CV_CONST_HANDLER
ZEND_ADD_SPEC_CV_TMPVAR_HANDLER
ZEND_ADD_SPEC_CV_CV_HANDLER
ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV_HANDLER
ZEND_ADD_LONG_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV_HANDLER
ZEND_ADD_DOUBLE_SPEC_CONST_TMPVARCV_HANDLER
ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV_HANDLER
7.1 で追加
比
較
特化したハンドラの威力
ハンドラの違いによる実装の比較
2017-10-08 JAPAN PHP CONFERENCE 2017 47
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL
ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *op1, *op2, *result;
op1 = EX_CONSTANT(opline->op1);
op2 = _get_zval_ptr_cv_undef(execute_data, opline->op2.var);
if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
result = EX_VAR(opline->result.var);
fast_long_add_function(result, op1, op2);
ZEND_VM_NEXT_OPCODE();
} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2));
ZEND_VM_NEXT_OPCODE();
}
} else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) {
if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, Z_DVAL_P(op1) + Z_DVAL_P(op2));
ZEND_VM_NEXT_OPCODE();
} else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
result = EX_VAR(opline->result.var);
ZVAL_DOUBLE(result, Z_DVAL_P(op1) + ((double)Z_LVAL_P(op2)));
ZEND_VM_NEXT_OPCODE();
}
}
SAVE_OPLINE();
if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) {
op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R);
}
if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) {
op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R);
}
add_function(EX_VAR(opline->result.var), op1, op2);
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
 従来のハンドラ (CONST_CV)  特化ハンドラ (LONG_NO_OVERFLOW)
static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL
ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *op1, *op2, *result;
op1 = EX_CONSTANT(opline->op1);
op2 = EX_VAR(opline->op2.var);
result = EX_VAR(opline->result.var);
ZVAL_LONG(result, Z_LVAL_P(op1) + Z_LVAL_P(op2));
ZEND_VM_NEXT_OPCODE();
}
PHP 7.2 の最適化器
2017-10-08 JAPAN PHP CONFERENCE 2017 48
PASS 9
PASS 1
PASS 2
PASS 3 PASS 10
PASS 4
→ PASS 16
PASS 11
PASS 5
PASS 13
未使用変数の除去
PASS 12
PASS 6 (データフロー解析)
PASS 7 (コールグラフの構築)
→ PASS 8
定数伝播
→ PASS 14
不要コード除去
PASS 8 定数伝播
定数式をコンパイル時に計算する
例:PHP 7.1 までは・・・
2017-10-08 JAPAN PHP CONFERENCE 2017 49
function test()
{
$a = 1 + 2;
$b = $a + 3;
$c = $b + 4;
echo $c;
}
compiled vars: !0=$a, !1=$b, !2=$c
op return operands
-------------------------------------
QM_ASSIGN !0 3
ADD !1 3, !0
ADD !2 4, !1
ECHO !2
RETURN null
 ソースコード  コンパイル結果
$a の右辺だけは計算されるが計算結果が後続の命令に伝わらない
PASS 8 定数伝播
定数式をコンパイル時に計算する
例:定数伝播を適用すると
2017-10-08 JAPAN PHP CONFERENCE 2017 50
function test()
{
$a = 1 + 2;
$b = $a + 3;
$c = $b + 4;
echo $c;
}
compiled vars: !0=$a, !1=$b, !2=$c
op return operands
-------------------------------------
QM_ASSIGN !0 3
QM_ASSIGN !1 6
QM_ASSIGN !2 10
ECHO !2
RETURN null
 ソースコード  コンパイル結果
PHP 7.2 で最適化レベルを指定
optimization_level=0x7fff8fff
$b や $c の右辺もコンパイル時に計算され定数になる
PASS 14 不要コードの除去
不要なバイトコード命令を除去する
例:不要コード除去も適用すると
2017-10-08 JAPAN PHP CONFERENCE 2017 51
function test()
{
$a = 1 + 2;
$b = $a + 3;
$c = $b + 4;
echo $c;
}
compiled vars: !0=$a, !1=$b, !2=$c
op return operands
-------------------------------------
ECHO 10
RETURN null
 ソースコード  コンパイル結果
PHP 7.2 で最適化レベルを指定
optimization_level=0x7fffafff
$a, $b, $c に代入する命令が除去され 10 が直接出力される
PASS 13 未使用変数の除去
使われることのない変数を除去する
例:未使用変数の除去も適用すると
2017-10-08 JAPAN PHP CONFERENCE 2017 52
function test()
{
$a = 1 + 2;
$b = $a + 3;
$c = $b + 4;
echo $c;
}
compiled vars: none
op return operands
-------------------------------------
ECHO 10
RETURN null
 ソースコード  コンパイル結果
PHP 7.2 のデフォルトの設定
optimization_level=0x7fffbfff
スタックに $a, $b, $c の領域を確保しなくなる (PASS 12 の処理)
もう少し複雑な例
2017-10-08 JAPAN PHP CONFERENCE 2017 53
# op return operands
----------------------------------
0 RETURN 10
 ソースコード  コンパイル結果
function test() {
$a = 1 + 2;
$b = $a + 3;
if ($b > 0) {
$a = 4;
} else {
$b = $a * 5.6;
}
return $a + $b;
}
まだ出来ていないこと
いろいろある
例:ループを含むコード
2017-10-08 JAPAN PHP CONFERENCE 2017 54
function test()
{
$a = 1;
for ($i=0; $i<3; ++$i) {
++$a;
}
echo $a;
}
# op return operands
------------------------------
1 QM_ASSIGN !0 1
2 QM_ASSIGN !1 0
3 JMP ->5
4 PRE_INC !0
5 PRE_INC !1
6 IS_SMALLER ~2 !1, 3
7 JMPNZ ~2, ->3
8 ECHO !0
9 RETURN null
 ソースコード  コンパイル結果
まとめ と 補足
PHP 7.1 以降で OPcache の最適化器が大きく進歩
コマンドラインからの PHP 実行で OPcache を有効にするには
2017-10-08 JAPAN PHP CONFERENCE 2017 55
$ php -d opcache.enable_cli=1 bench.php

OPcache の最適化器の今

  • 1.
    OPcache の最適化器の今 内山 雄司(@y__uti) Japan PHP Conference 2017
  • 2.
    自己紹介 内山 雄司 (@y__uti) ◦http://y-uti.hatenablog.jp/ (phpusers-ja) 仕事 ◦ 受託開発の会社 (株式会社ピコラボ) でプログラマをしています 興味 ◦ プログラミング言語処理系 ◦ 機械学習 2017-10-08 JAPAN PHP CONFERENCE 2017 2
  • 3.
    ベンチマークテスト 0.0 0.5 1.0 1.5 2.0 2.5 3.0 5.5.38 5.6.31 7.0.247.1.10 7.2.0RC3 実行時間[秒] PHP バージョン Zend/bench.php の実行時間 (各 5 回の実行の平均) OPcache 無効 OPcache 有効 2017-10-08 JAPAN PHP CONFERENCE 2017 3 Core i5-3337U CentOS 7 (VM 上の Guest OS)
  • 4.
    ベンチマークテスト 0.0 0.5 1.0 1.5 2.0 2.5 3.0 5.5.38 5.6.31 7.0.247.1.10 7.2.0RC3 実行時間[秒] PHP バージョン Zend/bench.php の実行時間 (各 5 回の実行の平均) OPcache 無効 OPcache 有効 2017-10-08 JAPAN PHP CONFERENCE 2017 4 Core i5-3337U CentOS 7 (VM 上の Guest OS) PHP 7 で高速化
  • 5.
    ベンチマークテスト 0.0 0.5 1.0 1.5 2.0 2.5 3.0 5.5.38 5.6.31 7.0.247.1.10 7.2.0RC3 実行時間[秒] PHP バージョン Zend/bench.php の実行時間 (各 5 回の実行の平均) OPcache 無効 OPcache 有効 2017-10-08 JAPAN PHP CONFERENCE 2017 5 Core i5-3337U CentOS 7 (VM 上の Guest OS) OPcache 有効時 PHP 7.1 以降も速度向上
  • 6.
    発表の流れ ベンチマーク結果の紹介 PHP のプログラム実行と OPcache OPcacheの最適化器の変遷 ◦ PHP 5.5 ~ 7.0 ◦ PHP 7.1 ◦ PHP 7.2 2017-10-08 JAPAN PHP CONFERENCE 2017 6
  • 7.
    PHP プログラムの実行方式 バイトコードインタプリタ方式 2017-10-08 JAPANPHP CONFERENCE 2017 7 PHP ファイルをコンパイルする インタプリタで実行する
  • 8.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 8 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 9.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 9 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 10.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 10 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 11.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 11 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 12.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 12 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 13.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 13 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 14.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 14 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 15.
    プログラム実行の様子 プログラムをバイトコード命令列にコンパイルして逐次実行 2017-10-08 JAPAN PHPCONFERENCE 2017 15 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 16.
    OPcache の役割 バイトコード命令をキャッシュして再利用 2017-10-08 JAPANPHP CONFERENCE 2017 16 バイトコードをキャッシュする PHP ファイルをコンパイルする キャッシュから バイトコードを取得する キャッシュ済み? インタプリタで実行する NO YES
  • 17.
    OPcache の役割 バイトコード命令を最適化する 2017-10-08 JAPANPHP CONFERENCE 2017 17 キャッシュから バイトコードを取得する キャッシュ済み? インタプリタで実行する NO YES バイトコードを最適化する バイトコードをキャッシュする PHP ファイルをコンパイルする
  • 18.
    発表の流れ ベンチマーク結果の紹介 PHP のプログラム実行と OPcache OPcacheの最適化器の変遷 ◦ PHP 5.5 ~ 7.0 ◦ PHP 7.1 ◦ PHP 7.2 2017-10-08 JAPAN PHP CONFERENCE 2017 18
  • 19.
    PHP 5.5 の最適化器 2017-10-08JAPAN PHP CONFERENCE 2017 19 PASS 3 連続するジャンプの短絡など PASS 10 NOP 命令の除去 PASS 9 一時変数のメモリ領域共有 PASS 5 制御フローグラフを用いた最適化 PASS 2 条件分岐の置き換えなど PASS 1 定数式の置き換えなど
  • 20.
    素朴な最適化の例 出発点:最適化を行わない場合 2017-10-08 JAPAN PHPCONFERENCE 2017 20 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 21.
    PASS 1 定数式の置き換え 1+ 2 > 0 は true だと分かるので・・・ 2017-10-08 JAPAN PHP CONFERENCE 2017 21 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 22.
    PASS 1 定数式の置き換え コンパイル時に計算してしまう 2017-10-08JAPAN PHP CONFERENCE 2017 22 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 JMPZ <bool>, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 23.
    PASS 2 条件分岐の置き換え if文の true/false はコンパイル時に決まっているので・・・ 2017-10-08 JAPAN PHP CONFERENCE 2017 23 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 JMPZ <bool>, ->8 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 24.
    PASS 2 条件分岐の置き換え NOP(飛ばない場合) か JMP (飛ぶ場合) に置き換える 2017-10-08 JAPAN PHP CONFERENCE 2017 24 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 NOP 3 JMP ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 25.
    PASS 3 連続ジャンプの短絡 JMP命令の飛び先も JMP 命令なので・・・ 2017-10-08 JAPAN PHP CONFERENCE 2017 25 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 NOP 3 JMP ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 26.
    PASS 3 連続ジャンプの短絡 最後の飛び先まで一気にジャンプする 2017-10-08JAPAN PHP CONFERENCE 2017 26 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 NOP 3 JMP ->6 4 ECHO 'A' 5 JMP ->9 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 27.
    PASS 10 NOP命令の除去 NOP (何もしない命令) を実行しても何もしないのだから・・・ 2017-10-08 JAPAN PHP CONFERENCE 2017 27 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 NOP 1 NOP 2 NOP 3 JMP ->6 4 ECHO 'A' 5 JMP ->9 6 ECHO 'B' 7 JMP ->9 8 ECHO 'C' 9 RETURN null  ソースコード  コンパイル結果
  • 28.
    PASS 10 NOP命令の除去 消す 2017-10-08 JAPAN PHP CONFERENCE 2017 28 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 JMP ->3 1 ECHO 'A' 2 JMP ->6 3 ECHO 'B' 4 JMP ->6 5 ECHO 'C' 6 RETURN null  ソースコード  コンパイル結果
  • 29.
    PASS 5 制御フローグラフ 処理の流れをグラフ構造で表現して各種最適化を行う 2017-10-08JAPAN PHP CONFERENCE 2017 29 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8  ソースコード  制御フローグラフ 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 9 RETURN null 8 ECHO 'C'
  • 30.
    PASS 5 制御フローグラフ この例ではどこを通るか決まっているので・・・ 2017-10-08JAPAN PHP CONFERENCE 2017 30 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } 0 ADD ~0 1, 2 1 IS_SMALLER ~1 0, ~0 2 JMPZ ~1, ->8  ソースコード  制御フローグラフ 3 JMPZ <bool>, ->6 4 ECHO 'A' 5 JMP ->7 6 ECHO 'B' 7 JMP ->9 9 RETURN null 8 ECHO 'C'
  • 31.
    PASS 5 制御フローグラフ ここまで最適化される(PHP 5.5 の頃からこの程度はできていた) 2017-10-08 JAPAN PHP CONFERENCE 2017 31 function test() { if (1 + 2 > 0) { if (false) { echo "A"; } else { echo "B"; } } else { echo "C"; } } # op return operands ---------------------------------- 0 ECHO 'B' 1 RETURN null  ソースコード  コンパイル結果
  • 32.
    PHP 5.6 の最適化器 2017-10-08JAPAN PHP CONFERENCE 2017 32 PASS 4 関数呼び出しの効率化 PASS 11 未使用リテラルの除去 PASS 9 PASS 5PASS 1 PASS 2 PASS 3 PASS 10
  • 33.
    PHP 7.0 の最適化器 2017-10-08JAPAN PHP CONFERENCE 2017 33 PASS 9 PASS 5PASS 1 PASS 2 PASS 3 PASS 10 PASS 4 PASS 11 PASS 12 関数スタックフレームの最適化
  • 34.
    PHP 7.1 の最適化器 2017-10-08JAPAN PHP CONFERENCE 2017 34 PASS 9 PASS 1 PASS 2 PASS 3 PASS 10 PASS 11 PASS 5 PASS 6 データフロー解析に基づく最適化 PASS 12 PASS 7 コールグラフの構築 PASS 4 (関数呼び出しの効率化) → PASS 16 関数のインライン化 (極めて限定的)
  • 35.
    PASS 7 コールグラフ 関数間の呼び出し関係をグラフ構造で表現して各種最適化を行う 2017-10-08JAPAN PHP CONFERENCE 2017 35 function test() { return f(); } function f() { return x() + y(); } function x() { ... } function y() { ... }  ソースコード  コールグラフ f test x y ただし現時点では解析結果を積極的には利用していない様子
  • 36.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 36 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  バイトコード命令列 (データフロー解析前) 0 ASSIGN !0, 3 1 ADD ~2 !0, 3 2 ASSIGN !1, ~2 3 IS_SMALLER ~2 0, !1 4 JMPZ ~2, ->7 5 ASSIGN !0, 4 6 JMP ->9 7 MUL ~2 !0, 5.6 8 ASSIGN !1, ~2 9 ADD ~2, !0, !1 10 RETURN ~2
  • 37.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 37 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ 9 ADD ~2, !0, !1 10 RETURN ~2 0 ASSIGN !0, 3 1 ADD ~2 !0, 3 2 ASSIGN !1, ~2 3 IS_SMALLER ~2 0, !1 4 JMPZ ~2, ->7 5 ASSIGN !0, 4 6 JMP ->9 7 MUL ~2 !0, 5.6 8 ASSIGN !1, ~2
  • 38.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 38 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ return $a + $b; $a = 3; $b = $a + 3; if ($b > 0) $a = 4; $b = $a * 5.6;
  • 39.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 39 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ return $a + $b; $a = 3; $b = $a + 3; if ($b > 0) $a = 4; $b = $a * 5.6;
  • 40.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 40 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ return $a + $b; $a = 3; $b = $a + 3; if ($b > 0) $a = 4; $b = $a * 5.6;
  • 41.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 41 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ $a <- $a or $a; return $a + $b; $a = 3; $b = $a + 3; if ($b > 0) $a = 4; $b = $a * 5.6; 静的単一代入形式 (SSA) と呼ばれる
  • 42.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 42 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ $a <- $a or $a; $b <- $b or $b; return $a + $b; $a = 3; $b = $a + 3; if ($b > 0) $a = 4; $b = $a * 5.6;
  • 43.
    PASS 6 データフロー解析 プログラムの各点でデータが取り得る型や値を解析する 2017-10-08JAPAN PHP CONFERENCE 2017 43 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }  ソースコード  制御フローグラフ $a <- $a or $a; // [3, 4]: int $b <- $b or $b; // ?: [int, float] return $a + $b; $a = 3; // 3: int $b = $a + 3; // 6: int if ($b > 0) // 4: int $a = 4; // ?: float $b = $a * 5.6;
  • 44.
    解析結果の用途 型や値に応じた効率的なバイトコード命令への変換 2017-10-08 JAPAN PHPCONFERENCE 2017 44 function test():int { $a = 1; $a = $a + 1; return $a; } # op return operands ---------------------------------- 0 ASSIGN !0, 1 1 ASSIGN_ADD !0, 1 2 VERIFY_RETURN_TYPE !0 3 RETURN !0  ソースコード  コンパイル結果 (データフロー解析なし) 0 QM_ASSIGN !0 1 1 PRE_INC !0 2 RETURN !0  コンパイル結果 (データフロー解析あり) PHP 7.1 時点ではそれほど積極的な実装はされていない様子
  • 45.
    解析結果の用途 型に特化したハンドラの選択 "ADD 命令" は大量にある(データの種類や型によって "適切な ADD" を選択) 2017-10-08 JAPAN PHP CONFERENCE 2017 45 ZEND_ADD_SPEC_CONST_CONST_HANDLER ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER ZEND_ADD_SPEC_CONST_CV_HANDLER ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER ZEND_ADD_SPEC_TMPVAR_CV_HANDLER ZEND_ADD_SPEC_CV_CONST_HANDLER ZEND_ADD_SPEC_CV_TMPVAR_HANDLER ZEND_ADD_SPEC_CV_CV_HANDLER ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV_HANDLER ZEND_ADD_LONG_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV_HANDLER ZEND_ADD_DOUBLE_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV_HANDLER 7.1 で追加
  • 46.
    解析結果の用途 型に特化したハンドラの選択 "ADD 命令" は大量にある(データの種類や型によって "適切な ADD" を選択) 2017-10-08 JAPAN PHP CONFERENCE 2017 46 ZEND_ADD_SPEC_CONST_CONST_HANDLER ZEND_ADD_SPEC_CONST_TMPVAR_HANDLER ZEND_ADD_SPEC_CONST_CV_HANDLER ZEND_ADD_SPEC_TMPVAR_CONST_HANDLER ZEND_ADD_SPEC_TMPVAR_TMPVAR_HANDLER ZEND_ADD_SPEC_TMPVAR_CV_HANDLER ZEND_ADD_SPEC_CV_CONST_HANDLER ZEND_ADD_SPEC_CV_TMPVAR_HANDLER ZEND_ADD_SPEC_CV_CV_HANDLER ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_LONG_NO_OVERFLOW_SPEC_TMPVARCV_TMPVARCV_HANDLER ZEND_ADD_LONG_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_LONG_SPEC_TMPVARCV_TMPVARCV_HANDLER ZEND_ADD_DOUBLE_SPEC_CONST_TMPVARCV_HANDLER ZEND_ADD_DOUBLE_SPEC_TMPVARCV_TMPVARCV_HANDLER 7.1 で追加 比 較
  • 47.
    特化したハンドラの威力 ハンドラの違いによる実装の比較 2017-10-08 JAPAN PHPCONFERENCE 2017 47 static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_SPEC_CONST_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *op1, *op2, *result; op1 = EX_CONSTANT(opline->op1); op2 = _get_zval_ptr_cv_undef(execute_data, opline->op2.var); if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) { if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) { result = EX_VAR(opline->result.var); fast_long_add_function(result, op1, op2); ZEND_VM_NEXT_OPCODE(); } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) { result = EX_VAR(opline->result.var); ZVAL_DOUBLE(result, ((double)Z_LVAL_P(op1)) + Z_DVAL_P(op2)); ZEND_VM_NEXT_OPCODE(); } } else if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_DOUBLE)) { if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_DOUBLE)) { result = EX_VAR(opline->result.var); ZVAL_DOUBLE(result, Z_DVAL_P(op1) + Z_DVAL_P(op2)); ZEND_VM_NEXT_OPCODE(); } else if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) { result = EX_VAR(opline->result.var); ZVAL_DOUBLE(result, Z_DVAL_P(op1) + ((double)Z_LVAL_P(op2))); ZEND_VM_NEXT_OPCODE(); } } SAVE_OPLINE(); if (IS_CONST == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op1) == IS_UNDEF)) { op1 = GET_OP1_UNDEF_CV(op1, BP_VAR_R); } if (IS_CV == IS_CV && UNEXPECTED(Z_TYPE_INFO_P(op2) == IS_UNDEF)) { op2 = GET_OP2_UNDEF_CV(op2, BP_VAR_R); } add_function(EX_VAR(opline->result.var), op1, op2); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); }  従来のハンドラ (CONST_CV)  特化ハンドラ (LONG_NO_OVERFLOW) static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_ADD_LONG_NO_OVERFLOW_SPEC_CONST_TMPVARCV_HANDLER(ZEND_OPCODE_HANDLER_ARGS) { USE_OPLINE zval *op1, *op2, *result; op1 = EX_CONSTANT(opline->op1); op2 = EX_VAR(opline->op2.var); result = EX_VAR(opline->result.var); ZVAL_LONG(result, Z_LVAL_P(op1) + Z_LVAL_P(op2)); ZEND_VM_NEXT_OPCODE(); }
  • 48.
    PHP 7.2 の最適化器 2017-10-08JAPAN PHP CONFERENCE 2017 48 PASS 9 PASS 1 PASS 2 PASS 3 PASS 10 PASS 4 → PASS 16 PASS 11 PASS 5 PASS 13 未使用変数の除去 PASS 12 PASS 6 (データフロー解析) PASS 7 (コールグラフの構築) → PASS 8 定数伝播 → PASS 14 不要コード除去
  • 49.
    PASS 8 定数伝播 定数式をコンパイル時に計算する 例:PHP7.1 までは・・・ 2017-10-08 JAPAN PHP CONFERENCE 2017 49 function test() { $a = 1 + 2; $b = $a + 3; $c = $b + 4; echo $c; } compiled vars: !0=$a, !1=$b, !2=$c op return operands ------------------------------------- QM_ASSIGN !0 3 ADD !1 3, !0 ADD !2 4, !1 ECHO !2 RETURN null  ソースコード  コンパイル結果 $a の右辺だけは計算されるが計算結果が後続の命令に伝わらない
  • 50.
    PASS 8 定数伝播 定数式をコンパイル時に計算する 例:定数伝播を適用すると 2017-10-08JAPAN PHP CONFERENCE 2017 50 function test() { $a = 1 + 2; $b = $a + 3; $c = $b + 4; echo $c; } compiled vars: !0=$a, !1=$b, !2=$c op return operands ------------------------------------- QM_ASSIGN !0 3 QM_ASSIGN !1 6 QM_ASSIGN !2 10 ECHO !2 RETURN null  ソースコード  コンパイル結果 PHP 7.2 で最適化レベルを指定 optimization_level=0x7fff8fff $b や $c の右辺もコンパイル時に計算され定数になる
  • 51.
    PASS 14 不要コードの除去 不要なバイトコード命令を除去する 例:不要コード除去も適用すると 2017-10-08JAPAN PHP CONFERENCE 2017 51 function test() { $a = 1 + 2; $b = $a + 3; $c = $b + 4; echo $c; } compiled vars: !0=$a, !1=$b, !2=$c op return operands ------------------------------------- ECHO 10 RETURN null  ソースコード  コンパイル結果 PHP 7.2 で最適化レベルを指定 optimization_level=0x7fffafff $a, $b, $c に代入する命令が除去され 10 が直接出力される
  • 52.
    PASS 13 未使用変数の除去 使われることのない変数を除去する 例:未使用変数の除去も適用すると 2017-10-08JAPAN PHP CONFERENCE 2017 52 function test() { $a = 1 + 2; $b = $a + 3; $c = $b + 4; echo $c; } compiled vars: none op return operands ------------------------------------- ECHO 10 RETURN null  ソースコード  コンパイル結果 PHP 7.2 のデフォルトの設定 optimization_level=0x7fffbfff スタックに $a, $b, $c の領域を確保しなくなる (PASS 12 の処理)
  • 53.
    もう少し複雑な例 2017-10-08 JAPAN PHPCONFERENCE 2017 53 # op return operands ---------------------------------- 0 RETURN 10  ソースコード  コンパイル結果 function test() { $a = 1 + 2; $b = $a + 3; if ($b > 0) { $a = 4; } else { $b = $a * 5.6; } return $a + $b; }
  • 54.
    まだ出来ていないこと いろいろある 例:ループを含むコード 2017-10-08 JAPAN PHPCONFERENCE 2017 54 function test() { $a = 1; for ($i=0; $i<3; ++$i) { ++$a; } echo $a; } # op return operands ------------------------------ 1 QM_ASSIGN !0 1 2 QM_ASSIGN !1 0 3 JMP ->5 4 PRE_INC !0 5 PRE_INC !1 6 IS_SMALLER ~2 !1, 3 7 JMPNZ ~2, ->3 8 ECHO !0 9 RETURN null  ソースコード  コンパイル結果
  • 55.
    まとめ と 補足 PHP7.1 以降で OPcache の最適化器が大きく進歩 コマンドラインからの PHP 実行で OPcache を有効にするには 2017-10-08 JAPAN PHP CONFERENCE 2017 55 $ php -d opcache.enable_cli=1 bench.php