PHP と srand と seed

億のレコード数を目指すサービスで、
レコードの uniq id を生成する PHP コードをみて絶望したっ!

function createUniqID() {
 srand( (double)microtime()*1000000 );
 <略>

それは勘弁して… ・゚・(ノД`)・゚・
(検索で見つけたどこかのブログからコピペしてきたそうで)

最近の PHP で srand を呼ぶのは同じ乱数列を再現させたい時位なので、
今回の場合は srand をただ削るだけですけど。

問題の解説

microtime() に対してこういう処理をすると、

  • microtime() => string(21) "0.49181000 1218109364"
  • (double)microtime() => float(0.49181)
  • (double)microtime()*1000000 => float(491810)

となるので、seed が 0 - 999,999 の間の数値に収まり、かつ、
srand の後に続く rand の乱数列がその seed で決まるので、
この関数が返す ID は 100万種に限定されてしまいます。

PHP5 から get_as_float の引数が導入されたので、 microtime(true)
とすれば、. が float の値で貰えて、もっとマシに出来ますが、
microtime が内部で呼ぶ gettimeofday がタスクのスイッチより分解能がある
という確信がない限り、これだけに頼る時点で危険な気がします。

そもそも PHP は結構前*1から srand を呼ばずに rand をいきなり呼んでも、
内部で勝手に srand 相当の処理が動くようになっています。

更に、この勝手に動く(seed を明示的に指定しない)srand の処理*2は、 (大雑把だけど)

1,000,000 * | (f1(sec xor usec) - f2(thread_id or pid ))*A |
xor unixtime * pid
MODMULT の処理が複雑なので f1, f2 でごまかし。^^;

の値を seed にしていて、下手な seed を渡すより、
これに任せた方がずっと良いです。
(pid と時刻の値を混ぜるのは、結構定石)

昔話

大昔の PHP では srand を明示的に呼ぶ必要があったので、

srand( (double)microtime()*1000000 );

というのは、よく使われていたようです。

microtime の仕様が今と異なるのか、はたまた、
そんなに沢山の乱数は要らないと思ってるのかワカラナイけど、
google で検索すると、このコードが沢山見つかります。

これはまずいと指摘する人もいるのですが、

Just using microtime() * 1000000 only results in 1000000 possible
seeds (and less on some platforms as noted) 

まだまだ、Web 上ではこの古いサンプルが目立つので
同じ間違いが色んな場所で繰り返されるんでしょうね。

*1:4.2.0 以降

*2:ext/standard/rand.c,lcg.c 参照の事。