nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

C言語でxorshiftの乱数を使う

1. はじめに

C言語でxorshiftという乱数発生を使うコードを作成しました。
前回のeastasianと同じように、nptから抜き出したものです。

配布はここ。

https://github.com/nptcl/hypd/tree/main/develop/random

最初にひどいことを言っておきますが、 私はこの分野の専門家ではないので、 このモジュールを使う利点は一切ないと思います。
こういう自分に自信のない書き込みというのはとても良くないとは思うのですが、 数値計算に関係するものならはっきり言っておきます。
何か問題が起きてからではどうしようもありません。
使うなら十分注意してくださいね。

使用するファイルは下記の通り。

  • random.c
  • random.h
  • md5encode.c
  • md5encode.h

md5encode.cは、単にMD5を計算するコードです。
xorshiftの初期値を計算するときのためだけに登録しました。
贅沢ですね。

2. コンパイル

コンパイルの方法は少し面倒です。
乱数の初期値を取得するために、OSを指定しなければなりません。

Unix, Linux, FreeBSDの場合は、次のようにしてください。

$ cc -DHYPD_UNIX -c random.c
$ cc -DHYPD_LINUX -c random.c
$ cc -DHYPD_FREEBSD -c random.c

上記3行は全部同じであり、 乱数の初期値を設定する際に/dev/urandomを読み込もうとします。

一方、Windows版も用意されています。
HYPD_WINDOWSdefineしてください。

# シェルでの実行例
$ cc -DHYPD_WINDOWS -c random.c

こちらは、advapi32.dllSystemFunction036という関数、 通称RtlGenRandomを使用して乱数の初期値を指定するようになります。

それ以外の場合でもコンパイルはできます。

$ cc -c random.c

ただし初期状態は、現在の時刻であったり、関数のポインターであったり、 極力努力はしますが、全然信頼できない初期値になりますので注意してください。
一応/dev/urandomを読み込もうとはしますが、 無かったら諦めた上で成功したことにしますので注意。

3. 使い方

使い方を説明します。

uint64_t x;
struct random_state state;

random_seed_string(&state, "Hello");
x = random_number_64bit(&state);

これで、変数xには乱数が返却されます。

変数stateには、乱数の内部状態が格納されます。
まずはrandom_seed_string関数を用いて、 Helloという文字列で内部状態を初期化します。
ここの文字列は何でもいいです。

そのあと、Helloに設定された内部状態を用いて、 random_number_64bitにより64bitの乱数を計算して返却します。

返却値を見てみましょう。

int main(void)
{
    uint64_t x;
    struct random_state state;

    random_seed_string(&state, "Hello");
    x = random_number_64bit(&state);
    printf("0x%" PRIX64 "\n", x);
    x = random_number_64bit(&state);
    printf("0x%" PRIX64 "\n", x);
    x = random_number_64bit(&state);
    printf("0x%" PRIX64 "\n", x);

    return 0;
}

出力結果は下記の通り。

0x6D16DA894C444233
0x7C8C640A1AD04940
0xBB5BBF5B4EF005AE

この結果は、どの環境でも同じになると思います。
何度実行しても同じ値になります。
別の乱数列を生成させたい場合は内部状態の初期値を変更します。

random_seed_string(&state, "AAABBB");
x = random_number_64bit(&state);

これで、内部状態AAABBBに対応する乱数列が生成されるようになります。

内部状態の設定は、文字列以外でも設定できます。
バッファを直接流し込んでみましょう。

random_seed_buffer(&state, "Hello", 5);

第1引数に内部状態、 第2引数にバッファのポインタ、 第3引数にバッファのサイズを指定します。
これは、

random_seed_string(&state, "Hello");

と同じですね。

あとは、OS依存になるのですが、 でたらめな内部状態を設定することもできます。

if (random_seed_randomly(&state)) {
    fprintf(stderr, "random_seed_randomly error.\n");
    exit(1);
}

このように設定すると、変数stateにはいつも違った値が設定されます。
つまり、実行するたびに違う乱数値が生成されるようになります。
ただしこれが利用できるのは一部の環境のみです。
詳しくはコンパイルの方法で説明しました。

乱数が生成されたのはいいのですが、 値の範囲が大きすぎて使いづらくて仕方がありません。
例えば、0から10までの値を取得したいときはどうしたらいいでしょうか。
間違っても%で余りを求めたらいけませんよ。
下記のように実行します。

x = random_equal_64bit(&state, 10);

ちなみに、次のようにも書けます。

x = random_less_64bit(&state, 11);

境界の1011が含まれるか含まれないかの違いなので、 どちらを使ってもいいです。

最後に浮動小数の乱数生成について説明します。

float a;
double b;
long double  c;

random_seed_string(&state, "Hello");
a = float_random_64bit(&state);
b = double_random_64bit(&state);
c = long_random_64bit(&state);

変数a, b, cには、 それぞれの型に対応する乱数が代入されます。
値の範囲は、0.0(含む)から1.0(含まれない)までです。
つまり0.0~0.9999...の乱数値です。
何でこんな範囲になったのかはANSI Common Lispの都合だと思います。

4. その他の情報

関数の名前が_64bitで終わっているものがあるのですが、 _32bitも用意されています。
違いは色々あるのですが些細なことなので、 全部_64bitだけでいいと思います。
たぶん返却値の型がuint32_tuint64_tという 違いだけなのでしょう。

変数stateは乱数の内部状態であり、 128bit長の整数が保存されています。
関数random_seed_stringrandom_seed_bufferは、 その引数からMD5を経由してstateの値を決定します。

何も文字列やバッファなんか使わなくても、 直接値を格納する方法もあります。

struct random_state state;
state.seed.u64[0] = 100;
state.seed.u64[1] = 200;

あるいはこんな感じ。

struct random_state state;
state.seed.u32[0] = 300;
state.seed.u32[1] = 400;
state.seed.u32[2] = 500;
state.seed.u32[3] = 600;

逆に言うなら、これらの値を保存しておけば、 いつでもその時の乱数列を再現できるというわけです。