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

PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪い

この投稿はPHP Advent Calendar 2013の12日目の記事です。

PHP恒例行事の参照と三項演算子のdisりですが、そろそろあさってな議論はやめませんかという話です。

今年のPHP-dis大賞といえばこちら。

PHPとかいう糞言語|いんまのブログ

※ 追記: これ書かれたのは2012年でしたすんません。

なんで君たちそんなコードが必要なのかね、と。結論から先言うと、きみたちがPHPが使えないって思うのは、そんな挙動に左右されるようなコードを書くからでしょ、だからCとかRubyとかそういう簡単な言語でわかった気になっている初心者はまったくもう...というわけでPHPの言語文法の基礎んとこ、いきますね。

まず、PHPのarrayは「値」です。もちろん文字列も「値」です。値は値なんだけど、それはミュータブルです。PHPのarrayもしくは文字列の代入は、一見すると、ポインタを使わない大きなC構造体を代入するような感じになります。

function x2first($arr)
{
    $arr[0] *= 2;
    return $arr;
}

$input = array(1, 2, 3, 4, 5);
$output = x2first($input);

// PHPの配列渡しはポインタではない
assert($input[0] != $output[0]);

PHPでは、コール先で中身を変更しても、コール元のスコープでは変数の値が維持されています。

で、ここでポインタわかって調子乗っちゃってるプログラマーは誤解するのですよ。大きな配列や文字列を別の変数に代入したり、関数に渡したりすると、その回数ぶん メモリの確保とバイナリのコピーが起こっている と。

その勘違いに捕らわれた人は、C++の参照演算子(ポインタを逆に表現した感じのアレ)を思い出して、「そうだPHPでも参照を使えばポインタと同じだ」と思い込んで、まあこんなコードを試すわけです。

function x2first_ref(&$arr)
{
    $arr[0] *= 2;
}

$input = array(1, 2, 3, 4, 5);
x2first_ref($input);
// 引数がコピーだということは、ポインタ相当のパフォーマンスを得るには参照渡しかな
assert($input[0] == 2);

やったーこれでメモリのコピー量がスタックに8バイト積むだけで済むぜ。

んなわけねーよ。

まあ、それほど極端でなかったとしても、みなさん計測せずに感情的な判断をしていませんか? パフォーマンスのために書いたつもりが、計測してないなんて、そんなの一度も実行してないのにバグってないロジックができたと言い張ってるようなものですよ。

ちゃんと計測します。

function profile($funcname)
{
    $bigarray = range(1, 1000000);

    echo $funcname . "\n";

    if ($funcname) {
        $start = microtime(true);
        $ret = $funcname($bigarray, 500000);
        $end = microtime(true);
    } else {
        $start = microtime(true);
        $end = microtime(true);
    }

    echo "  caller memory: " . number_format(memory_get_usage()) . "\n";
    echo "  time: " . (($end - $start) * 1000) . "(ms)\n";
    unset($bigarray);
    unset($ret);
}

ob_start();
profile(null); // ウォームアップ
ob_end_clean();

誰でも試せる素朴な計測ツールです。zval_とかわからなくても大丈夫な感じのやつ。100万要素の配列を準備して、処理にかかった時間と、呼び出し元スコープでの使用メモリを報告します。

まずは、本当に何もしない場合の数値を基準として取っておきましょうか。

profile(null); // 関数を呼ばない場合

// caller memory: 144,639,024
// time: 0.0030994415283203(ms)

実行するときは、たぶん php.ini のメモリ上限にひっかかるので、上限外してやりましょう。

$ php -d memory_limit=-1 test.php

さてじゃあ、引数渡しと戻り値の返却を、参照ありなしいろいろ試してみましょう。

// 参照なし
function nop_noref($arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡しのみ
function nop_ref_arg(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

// 参照渡し+参照返し
function &nop_ref_both(&$arr)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

profile('nop_noref');
profile('nop_ref_arg');
profile('nop_ref_both');

// nop_noref
//   callee memory: 144,639,072
//   caller memory: 144,639,072
//   time: 0.028133392333984(ms)

// nop_ref_arg
//   callee memory: 144,639,128
//   caller memory: 241,027,888 ← コール後にメモリ増大  
//   time: 106.05406761169(ms) ← なんだこれは

// nop_ref_both
//   callee memory: 144,639,112
//   caller memory: 144,639,112
//   time: 0.034093856811523(ms)

おい参照渡し、参照しない場合に対して処理時間が3700倍とかどないなっとんじゃい。

参照渡しのみした場合、見事にメモリが倍になっています。つまり巨大配列のコピーが発生した証拠です。実はこれ、参照返しをしなければ、戻り値の受け取りのとき、配列のコピーが生まれているのです。参照渡しだけでなく参照返しも意識しないと、こんなふうに内容が同じ100万要素の配列が2つできちゃう。こいつは厄介...

いや、厄介じゃないですね。驚くべきことに、いっさい参照を意識していないコールが最も優秀になっていますね。安全だし速いし。パフォーマンスのために内容破壊のリスクを冒して参照渡しするべき、とか考えてた人は残念でした。その努力には、まったく意味がありません。

いちど構造が作られてしまったら、多くの場合は読み取りアクセスだけで事足ります。読み取りに関しては、参照渡しだろうとそうでなかろうと、その負荷はまったく同じです。(7マイクロ秒の差は計測誤差です)

function getat_noref($arr, $index)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr[$index];
}

function getat_ref(&$arr, $index)
{
    echo "  callee memory: " . number_format(memory_get_usage()) . "\n";
    return $arr[$index];
}

profile('getat_noref');
profile('getat_ref');

// getat_noref
//   callee memory: 144,639,168
//   caller memory: 144,639,168
//   time: 0.03814697265625(ms)

// getat_ref
//   callee memory: 144,639,232
//   caller memory: 144,639,232
//   time: 0.030994415283203(ms)

参照にパフォーマンス上の意味はない、つまり、参照記号の & は、コール元に魔の手を延ばして値を書き換えてやるぞ〜、と待ち構えている怖いヤツの目印だというだけなんですよ。

もういちどポインタを思い出して。そう、PHPの変数は、最初からすべてポインタなのです。だから特別な記号を使わなくても、いくらでも変数を関数に引き渡せるのです。いやそうとしか説明できないでしょ、この結果見たら。

PHPで、すごく層の厚いフレームワークが案外実用的な速度で動く理由は、実はこのへんが効いているんですね。リクエストごとにやり直しでありながらも、言語ランタイムで代入が自動的にすべてポインタなので、層の間で無駄なコピーが発生していない。だからこそ、大きなarrayをコンフィグとして取り回したり、設計をあれだけ高級化しても案外ダメージが少ない。

でもまって、中で変更しても外は保護されてたアレはどう考えてもコピーでしょ。----そうですね、アレを試さなきゃ。

function x2at_noref($arr, $index)
{
    echo "  callee memory1: " . number_format(memory_get_usage()) . "\n";
    $arr[$index] *= 2;
    echo "  callee memory2: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

function &x2at_ref(&$arr, $index)
{
    echo "  callee memory1: " . number_format(memory_get_usage()) . "\n";
    $arr[$index] *= 2;
    echo "  callee memory2: " . number_format(memory_get_usage()) . "\n";
    return $arr;
}

profile('x2at_noref');
profile('x2at_ref');

// x2at_noref
//   callee memory1: 144,639,360
//   callee memory2: 241,028,168 ← ここでメモリを大幅に確保
//   caller memory: 241,028,168
//   time: 111.29093170166(ms) ← コピーコスト

// x2at_ref
//   callee memory1: 144,639,376
//   callee memory2: 144,639,376
//   caller memory: 144,639,376
//   time: 0.036954879760742(ms)

最初の nop_noref() に対して、この x2at_noref() がちょうど、中身を操作するかしないかの違いになっています。中身を操作しなければポインタのままだったものが、操作したことによって実体を共有できなくなると、こんどは裏で勝手にクローンが作られる、これがPHPの「普通の変数」の正体です。なんという高級言語。わざわざそういう最適化を書かなくても、言語ランタイムが暗黙的にやってくれてるんですよ。

こう見ると、参照のほうがむしろ普通に見えてきます。ただまあ、PHPの参照はいちど変数が参照になってしまうと、二度ともとに戻ることができないので、扱いにくくてやっぱりダメです。我々アプリケーションプログラマーにとっては、より平易で少ないコード量で、より安全で最適化された処理を得るのが正義なんですから。

zval の is_ref がどうとかあたりのちゃんとした説明は、2013-03-07 - bravewood の日記 で読めます。中身にこだわる方はどうぞ。参照代入演算子は、右辺の変数の特性をこっそり変化させてるあたりで、「うわなにこれ演算子の見た目に騙されてた」感を堪能できますよ。

まあ、そもそも話で、LLな言語の変数がミュータブルなのはしょうがないですが、であるからこそ、別のスコープではできるだけイミュータブルな値であるように意識して扱うのが、うまいプログラムのお作法ですよね。どこで中身が書き換えられるかわからない、副作用を期待した作りではなく、生成は生成に関わる箇所だけで、読み取りは参照透過で途中で言ってること変わらない、となっているのが、言語を問わず良いとされる方法です。

状態の変化は、それを意図したメソッドを持つオブジェクトでのみ起こるべきです。コードの中にできるだけ状態を作らない。わかりやすいインターフェースのオブジェクト設計で、これは状態だ、他は状態じゃないと静的に判別できるように考えましょう。今のPHPのオブジェクトインスタンスは、Javaのそれと同じく、明示的なクローンをするまで実体はひとつです。arrayを直接ではなく、可変な変数を持つオブジェクトを用意して、オブジェクトを普通に関数に渡し(オブジェクトは素直にポインタです)、そのオブジェクトのメソッドを使って変化を管理しましょう。

というわけで、参照渡しをカジュアルにやるのが間違いなのです。関数の戻り値の型の整合性がとれず、やむなく出力引数で表さなければならない場合などを除いて、基本的には使わない。使う意味がない。参照の仕様から来る複雑さに関しては、PHPが悪いというより、基礎を押さえずに用途を勘違いして使うほうが悪いと思います。PHPの変数の基礎を知っていれば、ほとんどの場合使わなくていいということが、おのずとわかると思います。

PHPが変な言語に見えるのは、そういう特殊な高級言語だと知らずに、素朴なメモリモデルを持つCのようなもので例えようとするのが良くないのです。もちろん、本当のビギナーが誤解して使っている場合も多いですが、よりややこしいのは、少々わかったふうな若葉マーク取れたぐらいの人が起こす勘違いです。そこに門外漢が弱いものいじめのように集まってきてしまう。本当のPHPプログラマーは、これがどういう言語なのかをよく知って、つつましく適切に、でも都合のいいところを活かして便利に使うのです。

まったく役に立たないかに見える参照の機能ですが、僕は、ここだけは使えるというホットスポットがあると思ってて、あとはそれを紹介しておきますね。

$str = '123-456';
preg_match('/^(.*?)-(.*?)/', $str, $match);

preg_match()の第三引数は参照渡しです。このコールで、$matchは宣言されている必要がありません。このように、コール元のスコープの変数の代入式と同じように働く参照渡しは、時として役に立ちます。

あと、クロージャが束縛する変数。

$removed = array();
$data = array_filter($data, function($element) use(&$removed) {
    if ($element->ckeck()) {
        return true;
    } else {
        $removed[] = $element;
        return false;
    }
});

$removed の参照を束縛しています。ここがもし参照でなかったら、まさに引数に配列を渡したときのように、クローンに対して操作され、$removed=array(); が維持されてしまいます。本来はオブジェクトを設けるべきなのかもしれませんが、クロージャがインラインで複雑さをさっと閉じ込めてくれることを思うと、こういう場合は、素朴なforeachループルーチンと置換えられるような手軽さが合っています。

個人的には、参照がありがたいと思って使うのはこれぐらいです。他に使い道ってあまりないなぁ。

あ、そうそう、参照を使ったら、忘れずに unset() するか、すぐにそのスコープを抜けること。変数がいちど参照になると、同じ名前でその変数名を再利用することができないのです。以後そこに代入するのは、参照先の領域への書き込みという意味になってしまい、新たな値を持つ変数を作るという意味にはなりません。

えーと、PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪いって言ってごめんなさいなので、歌って気分を晴らしてください

"仕様が理解らないの〜 なぜだどうしてだ〜 アホかー"

参考: http://www.youtube.com/watch?v=fZ-CM7n3F5c

明日は @ockeghem こと徳丸先生です。楽しみですね。