動的呼び出しとリファレンス

自分はメタプログラミング大好き人間なので、ついついクラスの動的生成とかメソッドの動的呼び出しをしてしまうのだが、
PHPでコーディングしていてふと気になったことがあったので調べてみた。


何が気になったかというと、それは、マニュアルを見た限りだとcall_user_func()はリファレンスでやり取りできなさそうだということ。


メソッドの動的呼び出しでリファレンスを戻すことはできるのか

こんな感じでテストしてみた。

<?php
/**
 * メソッドの動的呼び出しでリファレンスを戻すことはできるのかテスト。
 * ちなみに受け側をリファレンス受けにしない場合は当然ダメ。
 */
class Foo {
    var $property;
    function Foo() { $this->property = 'foo'; }
    function & get() { return $this->property; }
}

// 普通のメソッド呼び出し
// 期待通りに動くはず
echo "[Test A: 普通の呼び出し]\n";
$foo_a = new Foo();
$a_value =& $foo_a->get();
echo "\$a_value の値: " . $a_value . "\n";
echo "\$a_value に 'bar' を代入\n";
$a_value = 'bar';
echo "\$foo_a->property の値: " . $foo_a->property . "\n";

// call_user_funcはリファレンスを返さないのでだめぽい
echo "\n[Test B: call_user_funcによる呼び出し]\n";
$foo_b = new Foo();
$b_value =& call_user_func(array($foo_b, 'get'));
echo "\$b_value の値: " . $b_value . "\n";
echo "\$b_value に 'bar' を代入\n";
$b_value = 'bar';
echo "\$foo_b->property の値: " . $foo_b->property . "\n";

// 変数に括弧を付けて呼び出す方法(Perl風)
// こいつに賭けるしかない!(何を)
echo "\n[Test C: メソッド名を入れた変数で呼び出し]\n";
$foo_c = new Foo();
$method = 'get';
$c_value =& $foo_c->$method();
echo "\$c_value の値: " . $c_value . "\n";
echo "\$c_value に 'bar' を代入\n";
$c_value = 'bar';
echo "\$foo_c->property の値: " . $foo_c->property . "\n";

?>

結果

[Test A: 普通の呼び出し]
$a_value の値: foo
$a_value に 'bar' を代入
$foo_a->property の値: bar

[Test B: call_user_funcによる呼び出し]
$b_value の値: foo
$b_value に 'bar' を代入
$foo_b->property の値: foo

[Test C: メソッド名を入れた変数で呼び出し]
$c_value の値: foo
$c_value に 'bar' を代入
$foo_c->property の値: bar

Test Bの結果を見るに、やはりcall_user_func()はリファレンスを返してくれないようだ。
一方Test Cの方はFoo->propertyがしっかり'bar'になっているので、リファレンスとして扱えているようだ。
リファレンスが必要な場合はTest Cの方法を使うしかない。

メソッドの動的呼び出しでリファレンスを渡すことはできるのか

次は引数としてリファレンスを渡すことはできるのか調べてみた。

<?php
/**
 * メソッドの動的呼び出しでリファレンスを渡すことはできるのかテスト。
 */
function increment(&$value) {
    $value++;
}

// 普通のメソッド呼び出し
// 期待通りに動くはず
echo "[Test A: 普通の呼び出し]\n";
$a = 0;
echo "\$a の値(呼び出し前): $a\n";
increment($a);
echo "\$a の値(呼び出し後): $a\n";

// call_user_funcはリファレンスを受け取らないのでダメ
echo "\n[Test B-1: call_user_funcによる呼び出し(通常)]\n";
$b_1 = 0;
echo "\$b_1 の値(呼び出し前): $b_1\n";
call_user_func('increment', $b_1);
echo "\$b_1 の値(呼び出し後): $b_1\n";

// call_user_funcにリファレンスを突っ込んでみる
// 動くと思うけどやってはいけない方法
echo "\n[Test B-2: call_user_funcによる呼び出し(リファレンス渡し)]\n";
$b_2 = 0;
echo "\$b_2 の値(呼び出し前): $b_2\n";
call_user_func('increment', &$b_2);
echo "\$b_2 の値(呼び出し後): $b_2\n";

// call_user_func_arrayにラッピングして突っ込んでみる
echo "\n[Test B-3: call_user_func_arrayによる呼び出し]\n";
$b_3 = 0;
echo "\$b_3 の値(呼び出し前): $b_3\n";
call_user_func_array('increment', array(&$b_3));
echo "\$b_3 の値(呼び出し後): $b_3\n";

// 変数に括弧を付けて呼び出す方法(Perl風)
echo "\n[Test C: メソッド名を入れた変数で呼び出し]\n";
$c = 0;
echo "\$c の値(呼び出し前): $c\n";
$method = 'increment';
$method($c);
echo "\$c の値(呼び出し後): $c\n";

?>

結果

PHP Warning:  Call-time pass-by-reference has been deprecated - argument passed by value;  If you would like to pass it by reference, modify the declaration of call_user_func().  If you would like to enable call-time pass-by-reference, you can set allow_call_time_pass_reference to true in your INI file.  However, future versions may not support this any longer.  in /tmp/dynamic_call2.php on line 29

[Test A: 普通の呼び出し]
$a の値(呼び出し前): 0
$a の値(呼び出し後): 1

[Test B-1: call_user_funcによる呼び出し(通常)]
$b_1 の値(呼び出し前): 0
$b_1 の値(呼び出し後): 0

[Test B-2: call_user_funcによる呼び出し(リファレンス渡し)]
$b_2 の値(呼び出し前): 0
$b_2 の値(呼び出し後): 1

[Test B-3: call_user_func_arrayによる呼び出し]
$b_3 の値(呼び出し前): 0
$b_3 の値(呼び出し後): 1

[Test C: メソッド名を入れた変数で呼び出し]
$c の値(呼び出し前): 0
$c の値(呼び出し後): 1

Test B-1を見るに、やはりcall_user_func()ではリファレンスは扱えないようだ。
無理矢理リファレンスで渡してみたTest B-2は、期待通りの動作をしたもののWarningが出てしまった。
そういえば昔はこんな書き方をしていた時期があった気がする。ひどく紛らわしかったのを覚えている。
Test B-3はマニュアルにあった策で、これならばリファレンスをラッピングして渡せるので期待通りの動作になるとのこと。
そしてTest Cは先ほどと同じように期待通りの動作をしている。やはりこれしかないようだ。

動的呼び出しのベンチマーク

そんなこんなで色々調べていたら、call_user_func()は普通の呼び出しよりも倍ぐらい遅いという記事を見かけたので、ついでに簡単なベンチマークを取ってみた。
そりゃまあ多かれ少なかれオーバーヘッドはあるだろうとは思ってたけど、あまりにも重いようなら考え直さねば。

<?php
/**
 * 動的呼び出しのベンチマーク
 */
function increment(&$value) {
    $value++;
}

function timediff($ts, $te) {
    list($ts_usec, $ts_sec) = explode(' ', $ts);
    list($te_usec, $te_sec) = explode(' ', $te);
    return ((float)$te_usec - (float)$ts_usec) + (float)((int)$te_sec - (int)$ts_sec);
}

$loop = 10000;
printf("normal              : %f\n", benchmarkNormal());
printf("call_user_func      : %f\n", benchmarkCallUserFunc());
printf("call_user_func_array: %f\n", benchmarkCallUserFuncArray());
printf("variable            : %f\n", benchmarkVariable());


// 普通のメソッド呼び出し
function benchmarkNormal() {
    $start = microtime();
    $a = 0;
    for ($i = 0; $i < $GLOBALS['loop']; $i++) {
        increment($a);
    }
    $end = microtime();
    return timediff($start, $end);
}

// call_user_func
function benchmarkCallUserFunc() {
    $start = microtime();
    $a = 0;
    for ($i = 0; $i < $GLOBALS['loop']; $i++) {
        call_user_func('increment', $a);
    }
    $end = microtime();
    return timediff($start, $end);
}

// call_user_func_array
function benchmarkCallUserFuncArray() {
    $start = microtime();
    $a = 0;
    for ($i = 0; $i < $GLOBALS['loop']; $i++) {
        call_user_func_array('increment', array(&$a));
    }
    $end = microtime();
    return timediff($start, $end);
}

// 変数に括弧を付けて呼び出す方法(Perl風)
function benchmarkVariable() {
    $start = microtime();
    $method = 'increment';
    $a = 0;
    for ($i = 0; $i < $GLOBALS['loop']; $i++) {
        $method($a);
    }
    $end = microtime();
    return timediff($start, $end);
}

?>

結果

normal              : 0.018021
call_user_func      : 0.031089
call_user_func_array: 0.035807
variable            : 0.022216

なるほど確かにcall_user_func()は遅いようだ。
しかし変数にメソッド名を突っ込んで呼ぶ方法は、通常に比べると遅いものの、call_user_func()よりは速いようだ。
理屈はよくわからない(call_user_func()自体の呼び出しオーバーヘッド?)が、特に理由がない限りはcall_user_func()は使わない方がいいという結論で良いのだろうか……。