PHPの mt_rand() で生成される乱数はどのように初期化されているのか?
疑似乱数を使う時は、まず乱数に「種」(初期値)を与えて初期化しなければならないのですが、mt_srand()のマニュアルを読むと、
Note: As of PHP 4.2.0, there is no need to seed the random number generator with srand() or mt_srand() as this is now done automatically.
とあります。「乱数が初期化されていない場合、ランダムな値で初期化される」ということなので、大変便利に見えるのですが、どうも生成される乱数の分布が偏ってる気がする。ということで、初期化に使われる「ランダムな値」というのはどの程度ランダムなのか? 気になって調べてみました。
## 前提条件
Webなので、コネクション毎に乱数は初期化されます。1コネクションでmt_rand()を呼ぶ回数はあまり多くない(数回程度)なので、乱数の初期値でほぼ分布が決まります。
あと、調べたのは PHP 5.4.17 のソースです。
## mt_rand()のソースを調べる
まず、mt_rand() のソースは ext/standard/rand.c にあります。乱数が初期化されていない時は、以下の部分で初期化されます。
if (!BG(mt_rand_is_seeded)) {
php_mt_srand(GENERATE_SEED() TSRMLS_CC);
}
GENERATE_SEED() が乱数の種に使われているので、これを見てみましょう。ext/standard/php_rand.h に以下のような定義があります。
#define GENERATE_SEED() (((long) (time(0) * getpid())) ^ ((long) (1000000.0 * php_combined_lcg(TSRMLS_C))))
time(0) * getpid() と (1000000.0 * php_combined_lcg(TSRMLS_C)) の XOR を取っているわけですが、前者は現在時刻とプロセスIDなので、十分ランダムと言えるか疑問(分布の偏りもありそうだし、予測も難しくなさそう)。
次に php_combined_lcg() の方を見てみます。こちらは ext/standard/lcg.cにあります。
if (!LCG(seeded)) {
lcg_seed(TSRMLS_C);
}
で lcg_seed() を呼んでいるのですが、lcg_seed() の方はこんな感じ:
static void lcg_seed(TSRMLS_D) /* {{{ */
{
struct timeval tv;
if (gettimeofday(&tv, NULL) == 0) {
LCG(s1) = tv.tv_sec ^ (tv.tv_usec<<11);
} else {
LCG(s1) = 1;
}
#ifdef ZTS
LCG(s2) = (long) tsrm_thread_id();
#else
LCG(s2) = (long) getpid();
#endif
/* Add entropy to s2 by calling gettimeofday() again */
if (gettimeofday(&tv, NULL) == 0) {
LCG(s2) ^= (tv.tv_usec<<11);
}
LCG(seeded) = 1;
}
結局これも現在時刻とプロセスIDでした。
## /dev/urandomを使うには
もちろん、この程度で十分な場合もあると思いますが、あまり分布に偏りがあっては困る場合などは /dev/urandom を乱数の種に使いたいところです。
PHP 5.3以降では openssl_random_pseudo_bytes() が使えるので、以下のように乱数を初期化するのが良いでしょう。
mt_srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4))));
/dev/urandomを使った時の問題点として、エントロピーが枯渇した時に良い乱数が得られないというのがあります。これが問題になるような場合は、以下のようにチェックすると良いでしょう。
$strong = 0;
mt_srand(hexdec(bin2hex(openssl_random_pseudo_bytes(4, $strong))));
// $strong が 1であることをチェック。0 になっていると、良い乱数が返って来ていない。