読者です 読者をやめる 読者になる 読者になる

HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

PHPのrequire_onceが遅い話

PHP

もはや専門家の間では有名な話なのだろうが、今頃意識し始めて、ちょっと計ってみるかという気になったので計ってみる。

なんせ、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);
  • test2.php
    • spl_autoload_registerで似非クラスローダを登録
<?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

結果(グラフ)

生データでもなんとなく分かると思うが、一応並べ替えてグラフ化してみる。

f:id:hhelibex:20150721160945p:plain

まぁ、火を見るより明らかというか‥。自前クラスローダ版(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通りのコードを入れて試してみた。

  1. require('./dummy.php'); (「dummy.php」は空のファイル)
  2. defined("___{$i}___");

そうしたら、こんな感じになった。

  • require('./dummy.php');

f:id:hhelibex:20150723145451p:plain

  • defined("___{$i}___");

f:id:hhelibex:20150723153641p:plain

Zend Frameworkのソースを見ると、確かにメソッドの中で、例えば例外を投げる直前にrequire_once('Zend/Exception');していたりするので(そういうのが何箇所もある)、それは無駄だろうが、諸悪の根源とまではいかないようだ‥