
皆さんこんにちは、ガノー(Twitter:ganohr)です!
PHPのプログラミングにおいて、パフォーマンスチューニングをするときがあります。
その際に活用できる、実際の処理時間と使用メモリ料を計測するコードを共有します。
更新履歴
2023/05/14 公開
目次
- 1.PHPでパフォーマンス計測を行うコード
- 2.PHPでパフォーマンス計測を実際に行うサンプルコード
- 3.PHPのパフォーマンス計測に用いる関数の解説
- 3.1.PHPでパフォーマンス計測:メモリ使用量を取得するmemory_get_usage関数
- 3.2.PHPでパフォーマンス計測:ピークメモリ使用量を取得するmemory_get_peak_usage関数
- 3.3.PHPでピークメモリ使用量の値をリセットするmemory_reset_peak_usage関数
- 3.4.PHPでパフォーマンス計測:可能な限り高精度に現在時刻を取得するhrtime関数
- 3.5.PHPでパフォーマンス計測:マイクロ秒精度で現在時刻を取得するmicrotime関数
- 3.6.PHPでパフォーマンス計測:実行時間の求め方は終了時刻-開始時刻
- 4.PHPの時間取得関数hrtimeとmicrotimeの性能差はあるのか?
- 5.最後に
PHPでパフォーマンス計測を行うコード
<?php
function ganohrs_check_performance($func, $max = 1000000, $mes = "") {
echo "=== check performance $mes start ===" . PHP_EOL;
$t = 0;
if (function_exists("hrtime")) {
$t = hrtime(true);
} elseif (function_exists("microtime")) {
$t = microtime(true);
} else {
throw Exception("No Function for Time Measurement!");
}
memory_reset_peak_usage();
ganohrs_dump_memory();
for($i = 0; $i < $max; $i++) {
$func();
}
$p = 0;
if (function_exists("hrtime")) {
$p = (hrtime(true) - $t) / 1e+9;
} elseif (function_exists("microtime")) {
$p = (microtime(true) - $t);
} else {
$p = (time() - $p);
}
echo "performance: " . $p . PHP_EOL;
ganohrs_dump_memory();
echo "=== check performance $mes end ===" . PHP_EOL . PHP_EOL;
return $p;
}
function ganohrs_dump_memory() {
print "memory usage : ". (round(memory_get_usage ( ) / (2 ** 20) * 1000) / 1000) ."MB" . PHP_EOL;
print "memory usage real : ". (round(memory_get_usage (true) / (2 ** 20) * 1000) / 1000) ."MB" . PHP_EOL;
print "memory peak : ". (round(memory_get_peak_usage( ) / (2 ** 20) * 1000) / 1000) ."MB" . PHP_EOL;
print "memory peak real : ". (round(memory_get_peak_usage(true) / (2 ** 20) * 1000) / 1000) ."MB" . PHP_EOL;
}
PHPでパフォーマンス計測を実際に行うサンプルコード
<?php
ganohrs_check_performance(function() {
$v = eval("return 2**20;");
});
ganohrs_check_performance(function() {
$v = eval("return pow(2, 20);");
});
=== check performance start ===
memory usage : 0.39MB
memory usage real : 2MB
memory peak : 0.39MB
memory peak real : 2MB
performance: 1.1194066
memory usage : 0.391MB
memory usage real : 2MB
memory peak : 0.425MB
memory peak real : 2MB
=== check performance end ===
=== check performance start ===
memory usage : 0.391MB
memory usage real : 2MB
memory peak : 0.391MB
memory peak real : 2MB
performance: 1.6728114
memory usage : 0.391MB
memory usage real : 2MB
memory peak : 0.425MB
memory peak real : 2MB
=== check performance end ===
これは、phpでべき乗算を行うためのpow(n, m)
関数と、べき乗演算子n ** m
をeval("~")
で呼び出した際の、パフォーマンスの差を計測しています(Win 10 64bit、Ver 8.2.4)。
構文解析が間に挟まる場合、pow
関数を用いるよりも、**
演算子を利用するほうが顕著にパフォーマンスに優れることがわかります。
なお、これらをevalではなく直接記述しても、やはり**
演算子を用いる方がパフォーマンス的には優位です。
<?php
ganohrs_check_performance(function(){
$v = 2**20;
});
ganohrs_check_performance(function(){
$v = pow(2, 20);
});
=== check performance start ===
memory usage : 0.39MB
memory usage real : 2MB
memory peak : 0.39MB
memory peak real : 2MB
performance: 0.062826
memory usage : 0.39MB
memory usage real : 2MB
memory peak : 0.39MB
memory peak real : 2MB
=== check performance end ===
=== check performance start ===
memory usage : 0.39MB
memory usage real : 2MB
memory peak : 0.39MB
memory peak real : 2MB
performance: 0.0807358
memory usage : 0.39MB
memory usage real : 2MB
memory peak : 0.39MB
memory peak real : 2MB
=== check performance end ===
とはいえ、eval
を挟む場合とくらべ、速度にはさほど違いがないことも分かります。
このように、パフォーマンスチューニングにおいては、実際の処理時間やメモリ使用量を元に判断していく必要があります。
PHPのパフォーマンス計測に用いる関数の解説
上記のコードで利用しているPHPの関数群を見ていきましょう。
PHPでパフォーマンス計測:メモリ使用量を取得するmemory_get_usage関数
memory_get_usage
は、現在のプロセスが利用しているメモリ使用量を取得します。
引数を与えなかったり、false
を与えると、PHPが内部で利用しているメモリ使用量を出力します。
逆にtrue
を与えた場合、PHPが実際に割り当てているメモリ使用量を出力します。
PHPでパフォーマンス計測:ピークメモリ使用量を取得するmemory_get_peak_usage関数
memory_get_peak_usage
は、現在のプロセスが利用した最大メモリ使用量を取得します。
引数を与えなかったり、false
を与えると、PHPが内部で利用しているメモリ使用量を出力します。
逆にtrue
を与えた場合、PHPが実際に割り当てているメモリ使用量を出力します。
なお、パフォーマンス計測においては、次のmemory_reset_peak_usage
関数を併用する必要があります。
PHPでピークメモリ使用量の値をリセットするmemory_reset_peak_usage関数
memory_reset_peak_usage
は、現在のプロセスが利用した最大メモリ使用量をリセットします。
パフォーマンス計測においては、任意の区間のパフォーマンスを計測する必要があります。
したがって、パフォーマンス計測を開始する際に、memory_reset_peak_usage
関数を用いて予めリセットせねばなりません。
PHPでパフォーマンス計測:可能な限り高精度に現在時刻を取得するhrtime関数
hrtime
関数は、比較的新しい関数であり、7.3以降が対象になります。OS搭載の高精度な時間取得APIを用いて現在の時刻をマイクロ秒精度で取得します。
第一引数を省略するか、第一引数にfalse
を指定すると、戻り値は"現在のエポックタイムからの経過秒数" . " " . "現在の秒数から経過したマイクロ秒数"
という文字列を返却します。
パフォーマンス計測においては、これでは都合が悪いため第一引数にtrue
を指定して、マイクロ秒精度の値を示すfloat
値を取得します。
なお、このマイクロ秒を整数部をミリ秒、小数部をマイクロ秒にする場合は、hrtime(true) / 1e6
とし、整数部を秒、小数部をマイクロ秒含めた時間とするにはhrtime(true) / 1e9
とします。
なお、hrtime
関数はWindows版のPHPではバンドルされていますが、それ以外のOSではデフォルトではバンドルされていません。
LinuxベースのOSなどでは、PECL拡張モジュールから追加でインストールせねばなりません。
複数のOSで利用されうるパフォーマンス計測コードにおいては、function_exists
を用いて、予めhrtime
関数があるかどうか確認しておく必要があります。
またhrtime
関数を利用する場合、次節に掲載するmicrotime
関数との互換性の兼ね合いでhrtime(true) / 1e9
という変換をすることを推奨します。
PHPでパフォーマンス計測:マイクロ秒精度で現在時刻を取得するmicrotime関数
前述のhrtime
は比較的新しいため、まだまだ利用できる環境は多くありません。そのため多くの場合、代替としてmicrotime
関数が利用されます。この関数はPHP4以降標準搭載されています。
第一引数を省略するか、第一引数にfalse
を指定すると、戻り値は"現在の秒数からの経過マイクロ秒数" . " " . "エポックタイムから経過した現在の秒数"
という形式のstring
値を返却します。
パフォーマンス計測においては、これでは都合が悪いため第一引数にtrue
を指定して、整数部がエポックタイムからの経過秒数、小数部をマイクロ秒精度の値としたfloat
値を取得します。
なお、true
を指定した場合、前述のhrtime(true) / 1e9
の結果と同じ形式で値を処理できます。
PHPでパフォーマンス計測:実行時間の求め方は終了時刻-開始時刻
※ 再掲、先のコード例と同じ
<?php
function ganohrs_check_performance($func, $max = 1000000, $mes = "") {
echo "=== check performance $mes start ===" . PHP_EOL;
$t = 0;
if (function_exists("hrtime")) {
$t = hrtime(true);
} elseif (function_exists("microtime")) {
$t = microtime(true);
} else {
throw Exception("No Function for Time Measurement!");
}
memory_reset_peak_usage();
ganohrs_dump_memory();
for($i = 0; $i < $max; $i++) {
$func();
}
ganohrs_dump_memory();
$p = 0;
if (function_exists("hrtime")) {
$p = (hrtime(true) - $t) / 1e+9;
} elseif (function_exists("microtime")) {
$p = (microtime(true) - $t);
} else {
$p = (time() - $p);
}
echo "performance: " . $p . PHP_EOL;
echo "=== check performance $mes end ===" . PHP_EOL . PHP_EOL;
return $p;
}
先のコード例では、$t
が開始時刻、$p
が終了時刻と開始時刻の差(=実行時間)を計測しています。
このとき、hrtime
関数ととmicrotime
関数にtrue
を渡すことと、hrtime
関数は値 / 1e+9
としてmicrotime
関数の戻り値と互換性を確保しておくことが味噌です。
とはいえ、これ以上の説明は不要でしょう。
PHPの時間取得関数hrtimeとmicrotimeの性能差はあるのか?
さて、hrtime
関数とmicrotime
関数を見てきましたが、実際のところどれぐらい精度に差があるのでしょうか?
計測用に下記のコードを実行しました。
<?php
$p_arr = [];
for ($j = 0; $j <= 1; $j++) {
$p[$j] = [];
$sum1 = 0;
$sum2 = 0;
$sub1 = 0;
$sub2 = 0;
for ($i = 0; $i < 100; $i++) {
$check_func = $j === 0 ? "ganohrs_check_performance_hrtime" : "ganohrs_check_performance_microtime";
$p1 = call_user_func($check_func, function(){
$v = eval("return 2**20;");
}, 1000000, "$j : $i : 0");
$sum1 += $p1;
$p2 = call_user_func($check_func, function(){
c
}, 1000000, "$j : $i : 1");
$sum2 += $p2;
$p[$j][$i] = [$p1, $p2];
}
$ave1 = $sum1 * 0.1;
$ave2 = $sum2 * 0.1;
for ($i = 0; $i < 100; $i++) {
$sub1 += abs($ave1 - $p[$j][$i][0]);
$sub2 += abs($ave2 - $p[$j][$i][1]);
}
echo "Average1: $ave1, $sub1" . PHP_EOL;
echo "Average2: $ave2, $sub2" . PHP_EOL;
}
このコードは、少し早い$v = eval("return 2**20;");
と$v = eval("return pow(2, 20);");
を呼び出します。
また、それぞれ100万回呼び出すのを1回として、それを100回呼び出し、平均実行速度と、それぞれ100回分の実行速度の差の絶対値を加算して比較する処理を行っています。
要は1億回の計測を行い「どの程度平均実行速度とのばらつきがでるのか」を調べています。
実際の計測結果は以下のとおりです。
Average1: 1.13567593 , 0.016423698
Average2: 1.703622412 , 0.03369031544
Average1: 1.1413952374458, 0.020074790716171
Average2: 1.6897796916962, 0.025672821998596
※ 見やすくするため半角スペースを追加済み
※ 最初のAverage1
はhrtime
を利用して比較的早い関数の処理の計測結果
※ 最初のAverage2
はhrtime
を利用して比較的遅い関数の処理の計測結果
※ つぎのAverage1
はmicrotime
を利用して比較的早い関数の処理の計測結果
※ つぎのAverage2
はmicrotime
を利用して比較的遅い関数の処理の計測結果
その結果を見ると、別段hrtime
もmicrotime
もどっちもどっちです。
よくパフォーマンス計測ではOSの状況などに左右されがちですが、
- 他のアプリをすべて終了して計測している
- 100回の平均値とその差の平均を取っており、誤差はほとんど無視できる
hrtime
もmicrotime
も誤差の比率等に規則性がみあたらない
という条件&結果ですので、わざわざhrtime
を利用する利点はほとんど存在しないといっても良いでしょう。
ひとまずこれまでmicrotime
を利用してきた方はそのまま継続して利用し、今後hrtime
の精度が向上していこう、改めて利用すれば良いと思います。
最後に
パフォーマンス計測に誤差は付きものです。
そのため、誤差が少ないとされるhrtime
を使いたいと考えるのは至極当然です。
されど、実際に誤差を検証してみて、結局これまで同様誤差があることがわかりました。
ついつい理論上の話や、すでにある情報を元に意思決定しがちですが、パフォーマンス計測を実際に行ったり、誤差計測を行った上で判断すべきというのが、今回の検証で判明したことは重要です。
みなさんも今回共有したコードを活用して、パフォーマンス計測を行っていただければ幸いです。
以上、ガノー(Twitter:ganohr)でした!
コメントを書く