phpでmt_randに頼らず自前の乱数生成をする

phpで乱数といえば「一貫して壊れているmt_rand」の話が有名だが、
この記事はそれとは関係ない。が、間接的に関係している。

■この記事を一言で
mt_randを使わずにmt_randと同じ機能が欲しいので自作クラスにしたい

■結論
1.ここのソースを持ってくる
※php7.0以前で使いたい場合は、twist()は「一貫して壊れている」ものを使うこと。
 壊れているものは、$vが一箇所$uになっている。

2.randのrangeの実装が何故かmt_randと異なるので…

    function rand($min, $max)
    {
        return (int)($min + (($max - $min + 1) * ($this->int31() / 0x80000000)));
    }

以下のように直す。

    function rand($min, $max)
    {
        // phpの元のcソースを元に修正 https://github.com/php/php-src/blob/PHP-7.2.12/ext/standard/mt_rand.c
        $umax = $max - $min;
        return $this->rand_range32($umax) + $min;
    }

    function rand_range32($umax)
    {
        $result = $this->int32();

        /* Special case where no modulus is required */
        if ($umax == 0xffffffff) {
            return $result;
        }

        /* Increment the max so the range is inclusive of max */
        $umax++;

        /* Powers of two are not biased */
        if (($umax & ($umax - 1)) == 0) {
            return $result & ($umax - 1);
        }

        /* Ceiling under which UINT32_MAX % max == 0 */
        $limit = 0xffffffff - (0xffffffff % $umax) - 1;

        /* Discard numbers over the limit to avoid modulo bias */
        while ($result > $limit) {
            $result = $this->int32();
        }

        return $result % $umax;
    }

※実行速度はmt_randに比べて4倍〜5倍くらい遅い

■経緯
一貫して壊れているmt_randの修正についた一連のコメントの中で
「純粋な好奇心ですが、mt_randが返す値が変わると困るのはどんなとき?(意訳)」
「例えば迷路を生成するゲーム。同じ種からは同じ迷路を生成したい(超意訳)」
といったやりとりがあった。

私も困る人間の一人だったのだが、当然そんなリスクは承知の上で使っており、
「もし挙動が変わったら古い実装を書き起こせばいいや、そもそも本番環境のphpなんか更新せんやろ」と思ってたし、
実際にmt_randに依存していたサービスはphp7.1に更新することもなく寿命を全うした。

phpを使う人間なら、「phpが下位互換性の無いことをやらかしてくれる」リスクは考えておくべきだろう。

そして現在、改めて「同じ種からは同じ結果が生成されることを保障したい」シーンに遭遇してしまったので、
今度こそmt_randに頼るのはやめて、自前のソースにしようと考えたわけだ。ヒュー!意識高い!

まず最初に考えたのは「php 乱数 自作」でググって転がってるソースを使う、というもの。
mt_randと同じ出力がされることを確認できれば、信頼性は担保できるだろう。
ところがどっこい、全然ヒットしない。世の中には迷路を作ってる人間はいないのだろうか!

代わりにヒットするのはそう、「一貫して壊れているmt_rand」の件ばかり。
今は関係ないんだけど、知識としてちゃんと入れ直しておくか…と思ってそれを追っていくと、
なんと、phpでmt_randを実装したソースが見つかった。
これで解決…と思ったらそう簡単な話ではなかった。

mt_rand()と$mt->int31()…つまり最大の範囲で乱数を生成するぶんには
確かにmt_randと同じ値を返してくれる。しかし、
mt_rand(min, max)と$mt->rand(min, max)ではなんか異なる値が返ってくる。
正直よくわかんない、僕はメルセンヌツイスタをちゃんと理解したわけではない。

仕方ないのでphpのmt_randの元の実装(cのソース)を見て、
それと同じ実装をしなおしてあげたのが上記のソース、というわけ。
ちゃんとmt_randと同じ値を返してくれるようになった。

性能は、mt_randが10万回で0.005秒、$mt->randが10万回で0.025秒という感じ。

C言語 vs PHPじゃあ遅いに決まってるので仕方ないが、できれば早くしたいので、
すべての関数呼び出しを開いた(=該当箇所に同じソースをひたすらコピペした)ら0.020秒くらいにはなった。
ま、ここは汚いソースでも許されるだろう…。