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 になっていると、良い乱数が返って来ていない。
スポンサーリンク
スポンサーリンク:

フォローする