rand(3)の使い方(C/C++での擬似乱数取得方法)

どうも一部で微妙に盛り上がったらしい。

なので書いておく。

rand(3)の特徴

CやC++の標準ライブラリにはrand(3)という擬似乱数を生成する関数がある。

実装にもよるが、得られる擬似乱数の品質は、あくまでも「そこそこ」だ。もっと品質のよい擬似乱数を得る方法は色々とあるが、rand(3)は標準ライブラリ関数なので手軽かつポータブルだ。

C89やC99の仕様としては、rand(3)は一定の擬似乱数の並びを返す。つまり、rand(3)が返す擬似乱数は循環している。例えばFreeBSDのrand(3)の実装では、擬似乱数の下位12ビットが循環している*1。そのため、rand(3)の戻り値をある値で割った剰余を取得し続けていくと、ある時点において、最初の頃に得られた値が、以前と全く同じ順番で、再び出現する。

擬似乱数の循環の周期を含め、擬似乱数の品質は実装に依存する。Linuxでは(manの記述が正しいならば)rand(3)の擬似乱数の品質はよいが*2、これはむしろ例外だ。Mac OS X(というかFreeBSDを含めてBSD由来の実装を用いている環境)での「そこそこ」の品質の方が、本来のrand(3)の姿だろう。品質のよい擬似乱数を確実に得たいのなら、rand(3)を使うべきではない。

逆に言えば、rand(3)を使うのなら、得られる擬似乱数の品質が「そこそこ」であるという前提にもとづき、若干の小細工を弄する必要がある。

srand(3)

rand(3)が返す「一定の擬似乱数の並び」を切り替えるにはsrand(3)を使う。rand(3)やsrand(3)の実装が同一ならば、srand(3)の引数に異なる値を設定すればrand(3)が返す「一定の擬似乱数の並び」も異なるものとなるし、srand(3)に同じ値を設定すれば「一定の擬似乱数の並び」も同じものとなる。srand(3)を呼び出さずにrand(3)を呼び出した場合の挙動は、srand(3)の引数に1を設定した場合と同じだ。

srand(3)の効果は、あくまで「rand(3)が返す『一定の擬似乱数の並び』を切り替える」というものでしかない。srand(3)を何度も呼び出しても意味はない。何か特別な事情*3がない限り、srand(3)はプログラム中で1回呼び出せばよい。

一般的には、プログラム開始時に1回だけ、現在時刻(time(2)の戻り値)を引数としてsrand(3)を呼び出すことが多い。こうすると、プログラム実行時に「プログラムを起動した時刻」という異なる値でsrand(3)を呼びだすことになるため、プログラムを実行する度に異なる「一定の擬似乱数の並び」が選択されることを期待できる。

期待できるはずなのだが――実際には擬似乱数の品質が「そこそこ」な実装であるがために、rand(3)の使い方次第ではあるが、現在時刻が数秒異なる程度では大した効果が得られない環境が多かったりする。

srand(3)の引数として、現在時刻の代わりにプロセスIDを使用することもある。こうすると、プロセスごとに異なる「一定の擬似乱数の並び」が選択されることを期待できる。

または、MACアドレスのような「コンピュータごとに異なる値」をsrand(3)の引数に使用することで、機器ごとになるべく異なる「一定の擬似乱数の並び」が選択されることを期待する、ということもある。

もっとも、プロセスIDもMACアドレスも現在時刻と同様に、rand(3)の実装次第では少し値が異なる程度では効果がないことが多い。

プロセスIDやMACアドレスは環境依存の方法で取得するしかないので、標準ライブラリの範囲内で済ませたいなら、選択肢はtime(2)の戻り値しかない。

特定の範囲の擬似乱数を得たい場合

例えば0〜9の範囲で擬似乱数を得たいからといって、安直に次の方法を使うのは大間違いだ。

rand_val = rand() % 10;     /* NG */

理由は、大抵のrand(3)の実装では、得られる擬似乱数の下位ビットはそれほどランダムではないからだ。剰余を使う方法では、擬似乱数の品質は悪くなる。

上位ビットを用いるこちらの方法のほうがマシだ。

rand_val = (int) ((double) rand() / (RAND_MAX + 1.0) * 10);

浮動小数を使いたくないなら、この方法でもよいだろう。

rand_val = rand() / (RAND_MAX / 10 + 1);

もう少しこだわるなら、次の方法がある。

int x = (int) ((RAND_MAX + 1.0) / 10);
int y = x * 10;
int r;

do {
    r = rand();
} while (r >= y);

rand_val = r / x;

この方法は、「RAND_MAX + 1」が擬似乱数を得たい範囲の大きさ(ここでは10)で割り切れない場合に、得られる擬似乱数に偏りが生じる問題を回避するものだ。

……で、ここまで頑張っても問題がある。先にsrand(3)について書いた:

一般的には、プログラム開始時に1回だけ、現在時刻(time(2)の戻り値)を引数としてsrand(3)を呼び出すことが多い。こうすると、プログラム実行時に「プログラムを起動した時刻」という異なる値でsrand(3)を呼びだすことになるため、プログラムを実行する度に異なる「一定の擬似乱数の並び」が選択されることを期待できる。

期待できるはずなのだが――実際には擬似乱数の品質が「そこそこ」な実装であるがために、rand(3)の使い方次第ではあるが、現在時刻が数秒異なる程度では大した効果が得られない環境が多かったりする。

この問題ゆえに、例えばある日にrand(3)を使って生成した0〜9の範囲の擬似乱数の最初の値が毎度同じだとか、そういうことが起こりうる。

このようなケースに対応するために、事前にrand(3)で何回か擬似乱数を空読みするような工夫が必要となる。

より品質のよい擬似乱数を得たい場合

品質のよい擬似乱数を得たいなら、rand(3)を使ってはならない。

C++なら、C++11のstd::randomを使えば、手軽かつポータブルかつrand(3)よりマシだ。Unix環境なら、せめてPOSIXのrandom(3)/srandom(3)などを使うべきだろうし、BSD系ならarc4random(3)という手もある。Windows APIなら、CryptGenRandomがある。

まとめ

  • とりあえず、こう使え。
    1. プログラム起動時にsrand(3)を1回だけ呼び出す。
      • 引数には、現在時刻やMACアドレスなど、実行環境・タイミングによって違う値を設定する。
    2. 必要なだけrand(3)を呼び出す。
  • 特定の範囲(例えば0〜9)の擬似乱数を得たい場合は:
    • 安直に剰余を使うのではなく、小細工を弄する。
    • 予め何回かrand(3)を呼び出し、擬似乱数を空読みしておく。
  • rand(3)の擬似乱数の品質に期待したらダメ。よりよい品質の擬似乱数が欲しいなら、別の方法を採用する。

*1:FreeBSDのrandom(3)のmanより:「random() と srandom() 関数は、rand(3) と srand(3) 関数と (だいたい) 同じ呼び出し手順と初期化特性があります。rand(3) が作成するランダム数列がかなり劣ったものであることです。実際、rand が作成する下位の 12 ビットは、循環パターンになります。random() が作成するビットは、すべて有用です。たとえば `random()&01' では、ランダムなバイナリ値が生成されます。」

*2:random(3)と同様に、rand(3)でも非線形加法フィードバックを用いて擬似乱数を生成しているため。

*3:……といっても、まったく、さっぱり、何も思い浮かばないのだが。