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

HHeLiBeXの日記 正道編

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

メモリ喰らいの仕様

PHP

最近、気になる情報を目にしたらしい。

よく読んでみると、「ReflectionClass::getDocComment()」に関係する仕様らしい。

怖いので、3パターンほど試してみた。(コードがちょっと分かりづらくて申し訳ないものだが)実際に動かしたコードは後述する。

今回、実験に使用するコードでは、以下のことを行なう。

  1. 3パターンのPHPコードを動的に生成し、ファイルに保存する。
  2. それらのファイルを、順にinclude_onceする。
    • 実際に試してもらうと分かるが、include/include_once/require/require_onceのどれでも結果は変わらない。
  3. 増加したメモリ使用量を出力する。
    • memory_get_usage()と、念のためmemory_get_usage(true)の値の両方を出力。

3パターンのPHPコードとは以下のようなもの。

  • パターン1:「/* 〜 */」形式のコメントのみ
<?php 
/* 
@param hoge00000 00000
ここをどんどん増やしていく
*/
/*  */
class Hoge1 {}
  • パターン2:「/** 〜 */」形式のコメントの後に「/* */」を挟み込んでみる
<?php 
/**
@param hoge00000 00000
ここをどんどん増やしていく
*/
/*  */
class Hoge2 {}
  • パターン3:「/** 〜 */」形式のコメントの後に「/** */」を挟み込んでみる
<?php 
/**
@param hoge00000 00000
ここをどんどん増やしていく
*/
/** */
class Hoge3 {}

以下が、実際に動かしたコード。
各パターンでの、各クラスのドキュメントコメントも取得して、長さを出力してみている。

<?php

function _printMemory($msg) {
    static $pre1 = -1; // emalloc()が使用するメモリ(前回呼び出し時の値)
    static $pre2 = -1; // システムが割り当てたメモリ(前回呼び出し時の値)

    $mem1 = memory_get_usage();
    $mem2 = memory_get_usage(true);

    $diff1 = ($pre1 < 0 ? 0 : $mem1 - $pre1);
    $diff2 = ($pre2 < 0 ? 0 : $mem2 - $pre2);

    printf("%-16s: ", $msg);
    printf("%16d | %16d (%16d | %16d)\n", $mem1, $mem2, $diff1, $diff2);

    $pre1 = $mem1;
    $pre2 = $mem2;
}

_printMemory('init');

$dir = dirname($_SERVER['SCRIPT_FILENAME']);
if ($_GET) {
    if (isset($_GET['cnt'])) {
        $cnt = (int) $_GET['cnt'];
    } else {
        echo "Please specify GET parameter named 'cnt'";
        exit(1);
    }
} else {
    if ($argc != 2) {
        printf("Usage: %s <cnt>\n", $argv[0]);
        printf("\n");
        printf("    <cnt>      Count of tags in comment.\n");
        exit(1);
    }

    $cnt = (int) $argv[1];
}

$cmtBody = '';
for ($i = 0; $i < $cnt; ++$i) {
    $cmtBody .= sprintf("@param hoge%05d %05d\n", $i, $i);
}

$files = array(
    "{$dir}/foo-1.php",
    "{$dir}/foo-2.php",
    "{$dir}/foo-3.php",
);
$sources = array(
    array(
        '<?php ' . "\n" . '/* ' . "\n",
        '<?php ' . "\n" . '/**' . "\n",
        '<?php ' . "\n" . '/**' . "\n",
    ),
    $cmtBody,
    "*/\n",
    array(
        "/*  */\n",
        "/*  */\n",
        "/** */\n",
    ),
    array(
        'class Hoge1 {}' . "\n",
        'class Hoge2 {}' . "\n",
        'class Hoge3 {}' . "\n",
    ),
);
unset($cmtBody);

// clear contents of files.
foreach ($files as $file) {
    file_put_contents($file, '');
}
// put generated contents to files.
foreach ($sources as $source) {
    foreach ($files as $idx => $file) {
        if (is_array($source)) {
            $code = (count($source) <= $idx ? $source[count($source) - 1] : $source[$idx]);
        } else {
            $code = $source;
        }
        file_put_contents($file, $code, FILE_APPEND);
    }
}

_printMemory('start');

foreach ($files as $file) {
    include_once($file);

    _printMemory("after " . basename(${file}));
}

$rc1 = new ReflectionClass('Hoge1');
$rc2 = new ReflectionClass('Hoge2');
$rc3 = new ReflectionClass('Hoge3');

printf("%8d : %8s\n", strlen($rc1->getDocComment()), $rc1->getName());
printf("%8d : %8s\n", strlen($rc2->getDocComment()), $rc2->getName());
printf("%8d : %8s\n", strlen($rc3->getDocComment()), $rc3->getName());

こんな感じで実行する。(ちなみに、使用したのは仮想マシン上に構築したSientific Linux 6.2)

$ for i in 1 10 100 1000 10000 100000 ; do
    echo "=== ${i} ==="
    php hoge.php ${i}
done

実行結果。

=== 1 ===
init            :           651048 |           786432 (               0 |                0)
start           :           656336 |           786432 (            5288 |                0)
after foo-1.php :           657736 |           786432 (            1400 |                0)
after foo-2.php :           659184 |           786432 (            1448 |                0)
after foo-3.php :           660616 |           786432 (            1432 |                0)
       0 :    Hoge1
      29 :    Hoge2
       6 :    Hoge3
=== 10 ===
init            :           651048 |           786432 (               0 |                0)
start           :           656560 |           786432 (            5512 |                0)
after foo-1.php :           657960 |           786432 (            1400 |                0)
after foo-2.php :           659616 |           786432 (            1656 |                0)
after foo-3.php :           661048 |           786432 (            1432 |                0)
       0 :    Hoge1
     236 :    Hoge2
       6 :    Hoge3
=== 100 ===
init            :           651048 |           786432 (               0 |                0)
start           :           658632 |           786432 (            7584 |                0)
after foo-1.php :           660032 |           786432 (            1400 |                0)
after foo-2.php :           663760 |           786432 (            3728 |                0)
after foo-3.php :           665192 |           786432 (            1432 |                0)
       0 :    Hoge1
    2306 :    Hoge2
       6 :    Hoge3
=== 1000 ===
init            :           651048 |           786432 (               0 |                0)
start           :           679320 |           786432 (           28272 |                0)
after foo-1.php :           680720 |           786432 (            1400 |                0)
after foo-2.php :           705144 |           786432 (           24424 |                0)
after foo-3.php :           706576 |           786432 (            1432 |                0)
       0 :    Hoge1
   23006 :    Hoge2
       6 :    Hoge3
=== 10000 ===
init            :           651048 |           786432 (               0 |                0)
start           :           886320 |          1048576 (          235272 |           262144)
after foo-1.php :           887720 |          1310720 (            1400 |           262144)
after foo-2.php :          1119144 |          1572864 (          231424 |           262144)
after foo-3.php :          1120576 |          1835008 (            1432 |           262144)
       0 :    Hoge1
  230006 :    Hoge2
       6 :    Hoge3
=== 100000 ===
init            :           651048 |           786432 (               0 |                0)
start           :          2956320 |          3145728 (         2305272 |          2359296)
after foo-1.php :          2957720 |          3145728 (            1400 |                0)
after foo-2.php :          5259144 |          5505024 (         2301424 |          2359296)
after foo-3.php :          5260576 |          5505024 (            1432 |                0)
       0 :    Hoge1
 2300006 :    Hoge2
       6 :    Hoge3

まず目につくのが、パターン2とパターン3の違い。パターン3では空の「/** */」を挟み込むことによって、その上の長い「/** 〜 */」がドキュメントコメントではなくなり、切り捨てられるという動作が想像できる。
10万行というと、長過ぎるドキュメントコメントだが、例えばこれが、9個のメソッドを持つクラスで各メソッドとクラスにドキュメントコメントを5行ずつ持ち、そのようなクラスが20個ロードされるリクエストがあり、それが100リクエスト同時にくれば、ドキュメントコメントは10万行となる。

不幸なことに、ドキュメントコメントをリフレクションのためにメモリ上に保持する動作は、オフにすることができないらしい。
ということは、サーバーにアップする際にフィルタリングする仕組みを作り上げるか、IDEでの利便性を捨てて「/** 〜 */」形式のコメントを全て「/* 〜 */」に書き換えるかするしかない。
(ちなみに、上記のパターン3では、クラスのドキュメントコメントが空のものになるので、IDEでの利便性は確保できないものと推測している(未確認))

なんて迷惑な仕様なんだ‥orz