Rubyで乱数のシードを設定しないでランダムな数値を取得できるかRubyのコードを調査した
概要
Rubyはなぜ乱数のシードを入れないで乱数を生成できるのかRubyの言語自体のソースコードを軽く読んで調査した。
乱数を生成するクラスは乱数のシードを引数に入れないと、乱数のシードを生成するメソッドを呼び出す、その乱数を生成するロジック内ではCPUクロック数やOSの乱数生成を呼び出して利用して乱数のシードを生成していた。
乱数のシードがUNIX timeだけに依存したりしていないので、高アクセスなサービスや並列化したバッチ処理などで同時刻に乱数を生成してもシード値が同じでバラけないってことはなさそうだ。
はじめに
Rubyのランダムな数値を生成する場合はこのような書き方をする。
r=Random.new r.rand(10) => 5 引数に整数を入れると0-9までのランダムな数値(Integer)を出力する 引数を少数を入れるとFloat型でランダムな数値を出力する r.rand(5.5) => 1.81617029321715
randメソッドの仕様
Method: Random.rand — Documentation for core (2.7.0)
Rubyで乱数を生成するは、擬似乱数列生成器にメルセンヌ・ツイスタを利用している。擬似乱数発生装置は乱数のシードと呼ばれる初期値が必要だ。(シードが不要な擬似乱数発生装置もあるかもしれないが今回の主題ではない)
コンパイラを利用するプログラミング言語では、乱数を生成する際には乱数のシードを用意するものが多くインタプリタの場合はRubyのように乱数のシードを使わなくても乱数を生成することができるものが多い印象がある。
Ruby以外の他の言語の乱数生成ロジック
乱数のシードが必要な例としてGo言語の場合
package main import ( "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().Unix()) number:= rand.Int() fmt.Println(number) }
乱数のシードが不要な例としてJavaScriptの場合
const number = Math.random() console.log(number)
このJSのMath.random()の定義を見るとJavaScriptでは乱数のシードを入れられない逆にシードを入れさせないっていう仕様も珍しい
The implementation selects the initial seed to the random number generation algorithm; it cannot be chosen or reset by the user.
乱数のシードが不要な言語の場合、乱数のシードはどうしているのかRubyを例に調べてみる
Randomのコンストラクタの仕様を見ると Method: Random#initialize — Documentation for core (2.7.0)
Random.new_seedでランダムなシードの値を生成してインスタンス化しているしている。
Random.new_seedを実行してみるとこのような結果が返ってくる
Random.new_seed => 115032730400174366788466674494640623225
乱数のシード生成するのに乱数を生成している? ではその乱数を生成するシードはどう生成するというのだろうか?おそらく擬似乱数発生装置を利用しない方法で乱数を生成しているはずだ。
この対象のRubyのコード、Ruby本体のコードなのでC言語のコードで、Random.new_seedのコードは以下のように作られている。これを1行ずつ読み解く
Method: Random.new_seed — Documentation for core (2.7.0)
static VALUE random_seed(VALUE _) { VALUE v; uint32_t buf[DEFAULT_SEED_CNT+1]; fill_random_seed(buf, DEFAULT_SEED_CNT); v = make_seed_value(buf, DEFAULT_SEED_CNT); explicit_bzero(buf, DEFAULT_SEED_LEN); return v; }
1行目のVALUE v;
は6行目return v
で関数の返り値としているので生成されてたランダムな数値が格納される変数。
2行目uint32_t buf[DEFAULT_SEED_CNT+1];
はDEFAULT_SEED_CNTより1大きいサイズの配列を宣言、固定値DEFAULT_SEED_CNTのサイズは4
3行目fill_random_seed(buf, DEFAULT_SEED_CNT);
fill_random_seedで乱数のシードを作るための乱数のシードを作ってる。
このメソッドのロジックをざっくり読むと配列のサイズ4のseedの各要素にfill_random_bytes
でランダムなバイト数値を入れ、その上にCPU時間からナノ秒やマイクロ秒を取得してXORで上書きしている用に見える。
fill_random_bytesではちらっと見る限りLinuxのシステムコールで乱数を読んでるように見えが、今回は深追いしていない
static void fill_random_seed(uint32_t *seed, size_t cnt) { static int n = 0; #if defined HAVE_CLOCK_GETTIME struct timespec tv; #elif defined HAVE_GETTIMEOFDAY struct timeval tv; #endif size_t len = cnt * sizeof(*seed); memset(seed, 0, len); fill_random_bytes(seed, len, FALSE); #if defined HAVE_CLOCK_GETTIME clock_gettime(CLOCK_REALTIME, &tv); seed[0] ^= tv.tv_nsec; #elif defined HAVE_GETTIMEOFDAY gettimeofday(&tv, 0); seed[0] ^= tv.tv_usec; #endif seed[1] ^= (uint32_t)tv.tv_sec; #if SIZEOF_TIME_T > SIZEOF_INT seed[0] ^= (uint32_t)((time_t)tv.tv_sec >> SIZEOF_INT * CHAR_BIT); #endif seed[2] ^= getpid() ^ (n++ << 16); seed[3] ^= (uint32_t)(VALUE)&seed; #if SIZEOF_VOIDP > SIZEOF_INT seed[2] ^= (uint32_t)((VALUE)&seed >> SIZEOF_INT * CHAR_BIT); #endif }
4行目v = make_seed_value(buf, DEFAULT_SEED_CNT);
でbufに格納されている乱数のシードから乱数を生成している
make_seed_valueではbufに入っている配列の数値をrb_integer_unpackで結合して乱数を生成していると思う。
static VALUE make_seed_value(uint32_t *ptr, size_t len) { VALUE seed; if (ptr[len-1] <= 1) { /* set leading-zero-guard */ ptr[len++] = 1; } seed = rb_integer_unpack(ptr, len, sizeof(uint32_t), 0, INTEGER_PACK_LSWORD_FIRST|INTEGER_PACK_NATIVE_BYTE_ORDER); return seed; }
上記のロジックをまとめると、random_seedのロジックは配列を用意して、その配列にCPU時間など使ってランダムな数値を格納したあとに、配列を結合して乱数のシードを作っていると言える