コンユウメモ @kon_yu

作ったガラクタとか、旅行とかの話

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時間など使ってランダムな数値を格納したあとに、配列を結合して乱数のシードを作っていると言える

まとめ

  • Rubyの乱数の生成でシードデータが必要か否かを考えると、Rubyのコードを読む限り不要だと言える。必要なパターンがある気がするが今のところ思いつかない
  • 普段何気なく使っている乱数がどうやって実装されているのか垣間見ることができた
  • C言語読むの5年ぶりぐらいな気がする