PHPのrequire_onceが遅い話
もはや専門家の間では有名な話なのだろうが、今頃意識し始めて、ちょっと計ってみるかという気になったので計ってみる。
なんせ、Zend Frameworkのページでもパフォーマンスガイドとして書いてあるくらいだし。
計るにあたっては、「ノートPC上のVMでやりました」では話にならないだろうということで(それでも比率を見れば傾向はつかめると思うが)、とあるVPS上で計測をした。
ちなみに、「『Zend Frameworkと同じ命名規則でクラス名をつける』というルールの元で作ったものに対する計測」ってことでそのあたりはご注意を。
実験環境
環境はこんな感じ。
$ cat /etc/redhat-release CentOS release 6.6 (Final) $ uname -r 2.6.32-504.16.2.el6.x86_64 $ free total used free shared buffers cached Mem: 1922192 1480608 441584 536 202304 748092 -/+ buffers/cache: 530212 1391980 Swap: 2097148 6472 2090676 $ php -v PHP 5.3.3 (cli) (built: Jul 9 2015 17:39:00) Copyright (c) 1997-2010 The PHP Group Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies $ php -r 'var_dump(get_include_path());' string(32) ".:/usr/share/pear:/usr/share/php" $
ちなみにCPUは「Intel Xeon E312xx (Sandy Bridge)」の3コアらしい。
使用するプログラム
- App/Hoge0.php
<?php class App_Hoge0 { public static function test() {} }
- test0.php
- 1回だけrequire_onceする。
<?php printf("%s\n", __FILE__); $n = 1000000; if ($argc >= 2) { $n = (int)$argv[1]; } $startTime = microtime(true); require_once('App/Hoge0.php'); for ($i = 0; $i < $n; ++$i) { App_Hoge0::test(); } $endTime = microtime(true); printf("%8.5lf\n", $endTime - $startTime);
- test1.php
- ループのたびにrequire_onceする。
<?php printf("%s\n", __FILE__); $n = 1000000; if ($argc >= 2) { $n = (int)$argv[1]; } $startTime = microtime(true); for ($i = 0; $i < $n; ++$i) { require_once('App/Hoge0.php'); App_Hoge0::test(); } $endTime = microtime(true); printf("%8.5lf\n", $endTime - $startTime);
<?php printf("%s\n", __FILE__); $n = 1000000; if ($argc >= 2) { $n = (int)$argv[1]; } $startTime = microtime(true); function _myLoader($className) { return @include(str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'); } spl_autoload_register('_myLoader'); for ($i = 0; $i < $n; ++$i) { App_Hoge0::test(); } $endTime = microtime(true); printf("%8.5lf\n", $endTime - $startTime);
実行
面倒なので、以下のようなスクリプトを書いて実行。
for i in {1,2,5}{,00,000,0000,00000,000000} ; do printf "%d" $i for t in test0.php test1.php test2.php ; do php ${t} ${i} | grep -v /home/ | awk '{printf(",%s", $0);}' done printf "\n" done
結果(生データ)
1, 0.00011, 0.00011, 0.00014 100, 0.00013, 0.00030, 0.00016 1000, 0.00037, 0.00217, 0.00040 10000, 0.00264, 0.01997, 0.00298 100000, 0.02665, 0.20718, 0.02599 1000000, 0.24950, 1.90784, 0.25750 2, 0.00012, 0.00011, 0.00020 200, 0.00019, 0.00051, 0.00019 2000, 0.00062, 0.00419, 0.00065 20000, 0.00557, 0.04184, 0.00566 200000, 0.05527, 0.41456, 0.05114 2000000, 0.51434, 3.97734, 0.54890 5, 0.00014, 0.00013, 0.00015 500, 0.00025, 0.00227, 0.00036 5000, 0.00145, 0.00972, 0.00154 50000, 0.01421, 0.11185, 0.01292 500000, 0.13399, 0.98989, 0.13024 5000000, 1.27753,10.18646, 1.28340
結果(グラフ)
生データでもなんとなく分かると思うが、一応並べ替えてグラフ化してみる。
まぁ、火を見るより明らかというか‥。自前クラスローダ版(test2)が意外と健闘している。
そんなわけで(謎)、これからZend Framework本体のrequire_onceを消しまくる作業をしようと思う(と言っても、シェルコマンドのワンライナーで一括だが(謎))‥
2015/07/23追記
上記の検証だと、「require_onceが遅い」という証明にはならない気がしてきた。実行される命令数が違うのだから遅いのは当然。
ということで、test0.phpを以下のようにしてみた。
- test0.php
- 「何かの適当な処理」をする
<?php printf("%s\n", __FILE__); $n = 1000000; if ($argc >= 2) { $n = (int)$argv[1]; } $startTime = microtime(true); require_once('App/Hoge0.php'); for ($i = 0; $i < $n; ++$i) { 何かの適当な処理 App_Hoge0::test(); } $endTime = microtime(true); printf("%8.5lf\n", $endTime - $startTime);
「何かの適当な処理」には以下の2通りのコードを入れて試してみた。
require('./dummy.php');
(「dummy.php」は空のファイル)defined("___{$i}___");
そうしたら、こんな感じになった。
require('./dummy.php');
defined("___{$i}___");
Zend Frameworkのソースを見ると、確かにメソッドの中で、例えば例外を投げる直前にrequire_once('Zend/Exception');
していたりするので(そういうのが何箇所もある)、それは無駄だろうが、諸悪の根源とまではいかないようだ‥