Cでrandom()の状態を保存する。initstate()とsetstate()の使い方。


例えばsrandom(1)で初期化した後、random()を100万回呼んだときの状態を保存したい。pythonだと簡単にできるのだけど、Cだと難しい。

マニュアルにinitstate()とsetstate()という関数が載っているが、使い方がさっぱりわからなかった。検索すると「必要な回数ループしろ」なんていうダメ回答しか見つからない。仕方ないので頑張ってソースを読んだ。

アルゴリズムはわからないが、何をやってるかは理解できる。

  • initstate()で状態配列を初期化
  • setstate()で保存済の状態配列を設定
  • 状態配列の先頭に、乱数生成器のタイプと状態配列の位置情報がまとめて保存されている。
    • この値は、ソース内で state[-1] と書かれている。stateは状態配列の2個目の要素を指すポインタ。
  • random()を呼ぶたびに状態配列のポインタが動くが、state[-1]が更新されるわけではない。
  • sate[-1]が更新されるタイミングは、initstate()か、setstate()を呼んだ時。
    • initstate()は内部でsrandom()を呼んでしまう。状態を保存したいだけなのに、初期化する意味がない。
    • setstate()を呼ぶと、戻り値のstate[-1]が更新される。

要するに、setstateの戻り値が保存可能な状態配列。状態を保存したうえで引き続きrandom()を使うには、setstate(setstate(dummy)) とするとうまくいく。

char state[128];
char dummy[8];

initstate(seed, state, 128);

// ここでrandom()を必要なだけ呼ぶ。

setstate(setstate(dummy)); // 状態配列stateを更新。

// ここでstateを保存する。


ポインタを使った方がわかりやすいかもしれない。

char state_buffer[128];
char dummy[8];
char *state = state_buffer;

initstate(seed, state, 128);

// ここでrandom()を必要なだけ呼ぶ。

state = setstate(dummy); // stateを更新・待避。

// ここでstateを保存する。

setstate(state); // dummyからstateに戻す。


dummy を使わずに、いきなり setstate(state) としてもうまくいかない。

char state_buffer[128];
char *state = state_buffer;

initstate(seed, state, 128);

// ここでrandom()を必要なだけ呼ぶ。

state = setstate(state); // これはダメ

// これ以降、setstate(state)のせいでrandom()の値が変わってしまう。

setstate(state); // もう一回setstate()すれば復旧するが、これでは意味がわからなすぎる。


ソースのコメントには使用中のstateをsetstateに渡しても大丈夫と書いてあるが、少なくとも手元のOSXではうまくいかなかった。setstate()の冒頭部分を見るとダメな理由がわかる。更新前の状態配列で内部状態を上書きしている。

char *
setstate(arg_state)
        char *arg_state;
{
        register unsigned long *new_state = (unsigned long *)arg_state;
        register int type = new_state[0] % MAX_TYPES;                   
        register int rear = new_state[0] / MAX_TYPES; // ここで引数で渡された(未更新の)位置が取り込まれてしまう。
        char *ostate = (char *)(&state[-1]);
        if (rand_type == TYPE_0)
                state[-1] = rand_type;
        else
                state[-1] = MAX_TYPES * (rptr - state) + rand_type; // 戻り値のstate[-1]が更新される。これはOK。

        /* 中略 */

        state = &new_state[1]; // stateを入れ替え。
        if (rand_type != TYPE_0) {
                rptr = &state[rear]; // 上で取り込んだ未更新の位置が内部状態rptrに設定されてしまった。
                fptr = &state[(rear + rand_sep) % rand_deg];
        }
        end_ptr = &state[rand_deg];             /* set end_ptr too */
        return(ostate); // 古い方を返す。戻り値はOKだが、内部状態が正しくない。
}


デモ。

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv){
  char stat[128];       // 状態配列。
  char stat_saved[128]; // 状態配列を保存するデモ用。
  char stat_dummy[8];   // 状態配列ダミー。すぐ捨てる用。TYPE0、8バイト。

  initstate(1, stat, 128); // シード1、サイズ128(TYPE3)でstatを状態配列に使う。

  int i;
  for (i = 0; i < 1000000; i++) { // 100万回random()を呼んだ。
    random();
  }

  printf("stat[0] = %d\n", stat[0]); // 位置情報は更新されていない。TYPE3のまま。
  setstate(setstate(stat_dummy));
  printf("stat[0] = %d\n", stat[0]); // 更新された。

  // これでstatを保存できる。
  for(i = 0; i < 128; i++) {
    stat_saved[i] = stat[i];
  }

  printf("%ld\n", random());
  printf("%ld\n", random());

  // 状態をロード。上と同じ値が出力される。
  setstate(stat_saved);
  printf("* %ld\n", random());
  printf("* %ld\n", random());

  return 0;
}