hnwの日記

整数型のコピーオンライトは本当に遅いのか実験してみた

前回記事「PHP7はなぜ速いのか(zval編)」ではPHP7から内部的なデータ構造が大きく変わるという話題に触れました。具体的には、PHP5では全ての型の変数についてコピーオンライト方式を採用していたこと、この方式はデータサイズが大きいときに有効であること、またPHP7から真偽値型・整数型・浮動小数点数型の変数についてコピーオンライトをやめ、即値コピーするようになったことなどを紹介しました。


本稿ではデータサイズが小さいときのコピーオンライトが本当に非効率なのかどうか、PHP5とPHP7で実験してみます。

変数に別の変数を1億回代入してみる

まずは、次のプログラムを実行してみましょう。

<?php
function foo() {
    $x = 1;
    $a0 = $x;
    $a1 = $x;
    /*(略)*/
    $a99998 = $x;
    $a99999 = $x;
}
$t = microtime(true);
for ($i = 0; $i < 1000; $i++){
    foo();
}
var_dump(microtime(true)-$t);


上のプログラムで「(略)」となっている部分は10万行弱が省略されています。これは、関数fooの中で10万個の異なる変数に$xを代入し、この関数を1000回呼び出すのにかかった時間を測定するプログラムです。これをPHP 5.6.3とPHP 7のmasterブランチ最新版とで比較してみると、次のような結果になりました。

PHP5 PHP7 improvement
変数の代入 1.58s 1.43s x 1.10


PHP5の代入はコピーオンライトなので、10万回代入しても最初に作った変数$xの参照カウンタが10万回インクリメントされるだけです。一方、PHP7は即値コピーなので、代入のたびに16bytesのメモリコピーが行われます。PHP7の方が若干不利な条件にも思えますが、実際にはPHP7の方が1.1倍速いという結果になりました。


もっとも、この結果はPHP7の他の部分の改善が寄与している可能性もありますから、この結果だけでPHP7のデータ構造の方が確実に優れていると断言はできません。とはいえ、PHP5で参照カウンタの増減だけで済む状況であっても、即値コピーしているPHP7が互角以上だとは言えるでしょう。

変数に即値を1億回代入してみる

次に、先ほどと少し異なるプログラムを実行してみます。

<?php
function foo() {
    $x = 1;
    $a0 = 1;
    $a1 = 1;
    /*(略)*/
    $a99998 = 1;
    $a99999 = 1;
}
$t = microtime(true);
for ($i = 0; $i < 1000; $i++){
    foo();
}
var_dump(microtime(true)-$t);


今度は変数でなく即値1を1億回代入してみたところ、次のようにPHP7の方が3.87倍速いという結果になりました。

PHP5 PHP7 improvement
即値の代入 5.46s 1.41s x 3.87


このプログラムをPHP5で走らせたときの挙動を解説します。foo()が呼ばれると、未定義の変数10万個に即値を代入しようとします。このとき、代入のたびに動的にzval構造体をメモリ確保して整数型として値をセットすることになりますから、先ほどの例で参照カウンタを書き換えるだけだったのに比べて重い処理だと言えます。また、関数終了時にスコープを抜けるタイミングでこれらの値は全て破棄されますので、次回以降のfoo()呼び出しでも毎回同じ仕事をすることになります。


一方でPHP7の仕事内容は全く変わりません。先ほどの実験とブレがあるのは測定誤差だと思います。

まとめ

整数型のデータをコピーオンライト方式で管理するPHP5と即値コピーするPHP7とで速度を比較してみました。コピーオンライト方式で参照カウンタの増減だけで済む状況と比べても、データサイズが16bytesの即値コピーなら大差ないこと、また、新たなメモリ確保が必要な状況(即値代入もそうですし、コピーオンライトで変数値を書き換えたときに発生するコピーでも同じです)ではコピーオンライトの方が圧倒的に不利であることがわかりました。


というわけで、PHP7で真偽値型・整数型・浮動小数点数型のような小サイズの変数が即値コピーになったのは正解であろうことを実験で示せました。


蛇足かと思いますが、この結果を見て普段のPHPコードで即値の代入を減らそうかな?などとは考えないでください。というのも、今回の差は代入1回あたり40ナノ秒程度の差です。普段のPHPアプリケーションは10〜100ミリ秒程度で応答しているはずであり、10進で6桁くらい違う世界の話題になりますので、大抵のアプリケーションにとっては誤差でしかありません。少なくとも、コードの保守性を犠牲にするほどの価値は無いでしょう。