PHP では関数に値を渡す場合、以下の大きく3つの方法があります。各々の速度の違いが知りたいのです。
-
引数で渡す(arguments)
function myFunc($val) { echo $val; }
-
$GLOBALS
配列経由で渡す(globals)$GLOBALS['foo']='bar'; function myFunc($val) { $val=$GLOBALS['hoo']; echo $val; }
-
global
宣言経由で渡す(variables scope)$val='bar'; function myFunc($val) { globals $val; echo $val; }
速度やメモリ使用量を考えた場合、PHP7 では、どの方法が軽いのか気になったので、MacBookPro と RaspberryPi3 で測定してみました。
「関数 引数で渡す Globalsで渡す 速度」を Qiita に絞ってググってもドンピシャの情報が出てこなかったので、測定したことすら忘れてしまう未来の自分への備忘録として。
TL;DR(推測するなら、計測しろ)
子曰く
『パフォーマンスチューニングの鉄則は 推測するな、計測せよ である。』
(パフォーマンス |「モダンPHPアンチパターン」 @ Qiita より)
検証結果
PHP7.x + MacBookPro/RaspberryPi3+ では以下の結果となりました。
PHP7 では 100Mバイトのデータを100万回ぶん回しても 0.0n 秒の差。
∴ 「違いはほとんどない」ので、可読性やメンテナンス性を重視した方がよいレベル
- 「引数渡し」 >= 「
global
宣言渡し」 > 「$GLOBALS
配列渡し」(左から速度が速い順) - 「引数渡し」と「
global
宣言渡し」には、差は余りない - メモリの使用量は、いずれも余り違いはない
総括
(実際のアプリケーションではほとんどありえない前提での微々たる違いならば)
『私の結論としては、「書きやすいように(あるいは、事故を起こしにくいように)好きに書けばよい」です。』 (コメントより)
実は 「引数渡し = $GLOBALS
配列渡し >>> global
宣言渡し」という、「global
宣言渡しは圧倒的に遅い」といった StackOverflow の情報もあったので、最初は鵜呑みにしそうになりました。
しかし、他の情報のコメントを見ても議論が熱い割には噛み合っておらず、どの PHP のバージョンを語っていないことが原因と気づきました。
どうやら「$GLOBALS
配列渡し」が「global
宣言渡し」より速いというのは PHP5 時代の話らしく、PHP7 への移行の過渡期にある現在(2018/11)、バージョン情報や日付などのない情報には、やはり測定してみるものだと思いました。(ってか PHP7 速いなー)
合わせて読みたい
ボトルネックではないところの実行効率を上げても全体の最適化には繋がりません
TS;DR(変なとろこで細かいことが気になりすぎて伝わらない人の検証内容)
検証条件
$num_iterate = 1000000; //1回の for ループ回数(100万回)
$num_loop = 100; //平均を取るための回数(100万回の合計/100回)
$var_base = str_repeat('x', 100000000); // 95.37 Mバイト
処理内容の詳細は TS;DR をご覧ください。テストコード(全文)
<?php
// 値を関数へ引数渡し, $GLOBALS渡し, global宣言渡しする場合の違いの速度テスト
$var_base = str_repeat('x', 100000000);
$GLOBALS['var_global'] = $var_base;
function testfunc_param($var_param)
{
$var_local = $var_param;
return $var_local;
}
function testfunc_global()
{
global $var_global;
$var_local = $var_global;
return $var_local;
}
function testfunc_globalsarray()
{
$var_local = $GLOBALS['var_global'];
return $var_local;
}
function getValue($key, $array, $default = 0)
{
return isset($array[$key]) ? $array[$key] : $default;
}
function echoResults($title, $time = null, $memory = null)
{
static $results;
if (null === $time) {
print("{$title}\n\n");
foreach ($results as $title => $result) {
$ave_count = getValue('count', $result);
$ave_time = getValue('total_time', $result) / $ave_count;
$ave_memory = getValue('total_mem', $result) / $ave_count;
print("{$title}\nTime: {$ave_time}sec\nMemory: {$ave_memory}\n\n");
}
exit(0);
}
$result_each = getValue($title, $results, array());
$results[$title] = [
'count' => getValue('count', $result_each) + 1,
'total_time' => getValue('total_time', $result_each) + $time,
'total_mem' => getValue('total_mem', $result_each) + $memory,
];
print("{$title}\nTime: {$time} sec\nMemory: {$memory}\n\n");
}
$num_loop = 100;
$num_iterate = 1000000;
for ($loop = 0; $loop < $num_loop; $loop++) {
memory_get_usage();
$memoryStart = memory_get_usage();
$timeStart = microtime(true);
for ($i = 0; $i < $num_iterate; $i++) {
testfunc_param($var_base);
}
$timeEnd = microtime(true);
$memoryEnd = memory_get_peak_usage();
echoResults(
'Pass value by parameter',
$timeEnd - $timeStart,
$memoryEnd - $memoryStart
);
memory_get_usage();
$memoryStart = memory_get_usage();
$timeStart = microtime(true);
for ($i = 0; $i < $num_iterate; $i++) {
testfunc_global();
}
$timeEnd = microtime(true);
$memoryEnd = memory_get_peak_usage();
echoResults(
'Global var reference',
$timeEnd - $timeStart,
$memoryEnd - $memoryStart
);
memory_get_usage();
$memoryStart = memory_get_usage();
$timeStart = microtime(true);
for ($i = 0; $i < $num_iterate; $i++) {
testfunc_globalsarray();
}
$timeEnd = microtime(true);
$memoryEnd = memory_get_peak_usage();
echoResults(
'GLOBALS array reference',
$timeEnd - $timeStart,
$memoryEnd - $memoryStart
);
}
echoResults('* Total Average:');
- オンラインで動作を見る @ paiza.IO
検証環境:macOS HighSierra + PHP 7.2.6
$ date && sw_vers && system_profiler -detailLevel mini SPHardwareDataType && php -version
2018年 11月14日 水曜日 13時36分13秒 JST
ProductName: Mac OS X
ProductVersion: 10.13.6
BuildVersion: 17G3025
Hardware:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: MacBookPro12,1
Processor Name: Intel Core i5
Processor Speed: 2.7 GHz
Number of Processors: 1
Total Number of Cores: 2
L2 Cache (per Core): 256 KB
L3 Cache: 3 MB
Memory: 8 GB
Boot ROM Version: 180.0.0.0.0
SMC Version (system): 2.28f7
PHP 7.2.6 (cli) (built: May 25 2018 06:18:43) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.6, Copyright (c) 1999-2018, by Zend Technologies
検証環境:Raspbian Jessie + PHP 7.1.12
$ echo && date && cat /etc/os-release | head -1 && cat /proc/cpuinfo | grep -e "model name" -e "Hardware" -e "Revision" | uniq && cat /proc/meminfo |egrep -e "Active:|Inactive:|MemFree:" && php -v | head -1
2018年 11月 14日 水曜日 14:12:21 JST
PRETTY_NAME="Raspbian GNU/Linux 8 (jessie)"
model name : ARMv7 Processor rev 4 (v7l)
Hardware : BCM2835
Revision : a22082
MemFree: 157004 kB
Active: 438324 kB
Inactive: 275852 kB
PHP 7.1.12-1+0~20171129100725.11+jessie~1.gbp8ded15 (cli) (built: Dec 1 2017 04:22:35) ( NTS )
測定結果
* Total Average:
Pass value by parameter # 引数渡し(約 0.048 秒)
Time: 0.04791793346405 sec
Memory: 772.16
Global var reference # global 宣言渡し(約 0.053 秒)
Time: 0.05299122095108 sec
Memory: 779.12
GLOBALS array reference # $GLOBALS 配列渡し(約 0.066 秒)
Time: 0.066218328475952sec
Memory: 756.24
* Total Average:
Pass value by parameter # 引数渡し(約 0.394 秒)
Time: 0.39396689653397sec
Memory: 646.48
Global var reference # global 宣言渡し(約 0.433 秒)
Time: 0.43290241241455sec
Memory: 652.48
GLOBALS array reference # $GLOBALS 配列渡し(約 0.479 秒)
Time: 0.47904109477997sec
Memory: 629.28
コード解説
基本的に上記「検証条件」のテストコード(全文)と同じ内容ですが、少し噛み砕いて説明を知りたい方向け。
まずは 100 MB 近くの文字列を用意し、グローバル配列にセットします。
$var_base = str_repeat('x', 100000000); // 95.37MB
$GLOBALS['var_global'] = $var_base;
次に、「引数渡し」「global
宣言渡し」「$GLOBALS
配列渡し」の 3 つのテスト用関数を用意しました。
// 引数渡し
function testfunc_param($var_param)
{
$var_local = $var_param;
return $var_local;
}
// global 宣言渡し
function testfunc_global()
{
global $var_global;
$var_local = $var_global;
return $var_local;
}
// $GLOBALS 配列渡し
function testfunc_globalsarray()
{
$var_local = $GLOBALS['var_global'];
return $var_local;
}
次に関数をループする回数と、平均を取るための繰り返しの回数を用意しました。
$num_iterate = 1000000; //1回の for ループ回数(100万回)
$num_loop = 100; //平均を取るための回数(100万回の合計/100回)
1処理(1関数呼び出し)の、速度およびメモリ使用量の測定処理は以下の通り。
memory_get_usage(); // 空の呼び出し。ガーベジコレクションされるのか、これをしないと安定して測定できなかった
$memoryStart = memory_get_usage(); //現在のメモリ量取得
$timeStart = microtime(true); //現在の時間を取得
for ($i = 0; $i < $num_iterate; $i++) {
<テスト関数>(<$var_base>);
}
$timeEnd = microtime(true); //終了時間を取得
$memoryEnd = memory_get_peak_usage(); //終了時のメモリ量を取得
//1回ぶんの測定結果の記録と表示
echoResults(
'Pass value by parameter', //表示タイトル
$timeEnd - $timeStart, //使用時間
$memoryEnd - $memoryStart //使用メモリ
);
上記を 3 つのテスト関数ぶん繰り返し、平均を取るためにループさせます。
for ($loop = 0; $loop < $num_loop; $loop++) {
// 引数渡しのストレス・テスト
<$num_iterate ぶん繰り返し>
// global 宣言渡しのストレス・テスト
<$num_iterate ぶん繰り返し>
// $GLOBALS 配列渡しのストレス・テスト
<$num_iterate ぶん繰り返し>
}
// 測定結果の平均値の表示
echoResults('* Total Average:');
function echoResults($title, $time = null, $memory = null)
{
static $results;
//平均の表示
if (null === $time) {
print("{$title}\n\n");
foreach ($results as $title => $result) {
$ave_count = getValue('count', $result);
$ave_time = getValue('total_time', $result) / $ave_count;
$ave_memory = getValue('total_mem', $result) / $ave_count;
print("{$title}\nTime: {$ave_time}sec\nMemory: {$ave_memory}\n\n");
}
exit(0);
}
//処理結果の保存
$result_each = getValue($title, $results, array());
$results[$title] = [
'count' => getValue('count', $result_each) + 1,
'total_time' => getValue('total_time', $result_each) + $time,
'total_mem' => getValue('total_mem', $result_each) + $memory,
];
//1回ごとの処理結果の表示
print("{$title}\nTime: {$time} sec\nMemory: {$memory}\n\n");
}
所感
アプリの諸設定を色々な関数に渡して使いまわしたかったのです。
ここで言う諸設定とは、ファイルやディレクトリのパスや、フラグなどのことです。
その場合、クラス・オブジェクトにするのが一般的なのかもしれませんが、アプリの規模自体が小さいため、配列に諸設定を突っ込んで、関数(の引数)に渡していました。
functions myFunc1(array $settings)
{
// Do something with $settings
}
functions myFunc2(array $settings)
{
// Do something with $settings
}
途中から、ふと
「毎回、関数に引数で渡すの面倒だから、関数内で global $settings;
としちゃってもいいんじゃね?」
と邪念がよぎったのですが、
「下手に楽をしてパフォーマンスが落ちたらどうしよう」
と、パフォーマンスもヘッタクレも関係ない小さなアプリに対して悩んでしまったのです。
Qiita に絞ってググってもピンポイントで出てこなかったので、しぶしぶググったところ、全体としては「気にするな」的な意見が多いのですが、「$GLOBALS
の方が global
で宣言するよりも速い」「いや global
の方が速い」など、「C がそうだから」「理屈で考えてもそうだから」と、よく理解できない根拠を理由に意見が分かれていました。
「パフォーマンスチューニングの鉄則は 推測するな、計測せよ」という言葉を思い出していたところ、同じ議論を StackOverflow で見つけ「実測しようよ」というコメントを見つけたのです。
全体を読まずに「これは!」とコピペピピックしそうになったのですが、なんかそこでも「いや、俺の環境では違う」といったコメントがありました。どうやら PHP のバージョン違いっぽいです。
これはまさに「実際の運用と近い環境とユースケース(データ・処理)に対して行ってください。そうしないと、実運用環境では生じない誤った結論に導かれることがあります」に該当する内容です。
そこで、アプリの開発にも飽きてきたのでいささか気になったので測定してみようと思いたったわけです。
ところが、測定結果のメモを取っている時点で「えっとー、どっちが速いんだっけ?」となったので、「これは未来の俺は絶対に忘れる」と気づき「記事にせねば」と思い記事にしました。どう?未来の自分、思い出した?
参考文献
- 「Global variable or pass variable in PHP? (performance)」@ StackOverflow
- 「
memory_get_usage
」@ PHP マニュアル - 「
memory_get_peak_usage
」 @ PHP マニュアル