PHP にはランダムな値を取得する関数がいくつかあります。その中でよくテストデータ作成で使うのは以下の2つです。mt_rand は数値を取得し、random_bytes はバイト列を取得しよく文字列に変換します。この mt_rand 関数のランダム性の限界について紹介します。この記事では PHP 8.1 を扱います。
PHP: mt_rand – Manual
PHP: random_bytes – Manual
ざっくばらんに結論からいうと PHP のランダムな値は mt_getrandmax 関数で得られる値までの長さの範囲でしかランダム性を最大に取れません。例えばmt_rand(0, PHP_INT_MAX)で得られる値は期待した通りに散らばらない、とかそういう具合です。
PHP: mt_getrandmax – Manual
警告
minからmaxまでの幅を mt_getrandmax() の範囲内におさめる必要があります。
つまり、(max–min) <= mt_getrandmax()でなければいけないということです。この範囲をこえてしまうと、mt_rand() が返す値のランダム性が、本来あるべき姿よりも低くなってしまいます。
この限界は PHP の mt_rand 関数がとる値の範囲が 32bit で表現できる値に限られているために生まれている限界です。mt_rand 関数のランダムな値を得る部分の実装部が次です。
php-src/mt_rand.c at PHP-8.1.4 · php/php-src#L157
php-src/mt_rand.c at PHP-8.1.4 · php/php-src#L312
PHPAPI uint32_t php_mt_rand(void)
{
/* Pull a 32-bit integer from the generator state
Every other access function simply transforms the numbers extracted here */
uint32_t s1;
if (UNEXPECTED(!BG(mt_rand_is_seeded))) {
zend_long bytes;
if (php_random_bytes_silent(&bytes, sizeof(zend_long)) == FAILURE) {
bytes = GENERATE_SEED();
}
php_mt_srand(bytes);
}
if (BG(left) == 0) {
php_mt_reload();
}
--BG(left);
s1 = *BG(next)++;
s1 ^= (s1 >> 11);
s1 ^= (s1 << 7) & 0x9d2c5680U;
s1 ^= (s1 << 15) & 0xefc60000U;
return ( s1 ^ (s1 >> 18) );
}
uint32_t の値が返っています。これは unsigned integer 32bit type の略で 32bit の範囲で表せる 0 以上の整数であることを示します。mt_rand 関数のランダムな値はこの型のとれる値の範囲しか状態を持てないため、それ以上の範囲を mt_rand 関数の引数に渡しても思ったほどランダムになりません。もっともテストデータはざっくばらんなランダムで十分なのでそういう使い道ならば問題にはなりません。もしセキュリティ等で可能な限り強力な乱数が必要な場合は暗号論的に安全が保証されている random_int 関数を使いましょう。
ちなみに PHP 7.0 以前は恐ろしい偏り方をします。問題を誘発しかねないので気にする必要があります。
Online PHP editor | output for 5EGOi#v7.0.33
<?php
$loopCount = 1e6;
$min = 0;
$max = mt_getrandmax()*2;// 限界を超えた偶数
// 大量にランダムな値を作って奇数偶数の数を集計
$res = [
'odd' => 0,
'even'=> 0,
];
while ($loopCount--) {
$v = rand($min, $max);
$v%2 ? $res['odd']++ : $res['even']++;
}
// 集計結果を表示
var_dump($res);
/*
array(2) {
["odd"]=>
int(1000000)
["even"]=>
int(0)
}
*/