LoginSignup
1
1

More than 3 years have passed since last update.

C#とjavascriptで乱数系列を共有したいんだが…(XorShiftを題材として)

Posted at

端的に言ってしまうと…

javascriptでUInt32な数値を扱いたかったんです
ん?
いや、とっ掛かりはやはりXorShiftな訳で、ある乱数列にデータを重畳してプログラム間で受け渡ししたいな、と
まぁその様に思った次第です

でも今回のプログラム間、ってのがC# ⇔ javascriptなんです
ここで問題になるのがjavascriptの乱数はseedを必要としません、ってか「受け付けません」
それでは乱数列の共有など出来るハズもありませんから、何かseedを指定できる簡便な乱数発生器はないかな?

と思って行き当たったのがXorShiftなんです

XorShift

これはググって頂ければすぐ分かりますが、すんごい軽量ながらseedを元に結構ガチな乱数列を捻り出してくれる(らしい)疑似乱数発生器です

おーっ!
こいつぁ御誂え向き…

なんて思ったのも束の間
そこそこの周期を期待するなら、unsigned intな数値を扱える必要があるぅ?

いやいやいやー
御冗談を
javascriptの扱える数値に型なんて概念ありませんぜ
なんなら、整数と小数どころか数値列と文字列の境だって曖昧だっちゅーの

実装(C#)

ま、それはさておき、やってみましょに

まずはC#からですね

public  class XorShift {
    private uint t = 0u;
    private uint x = 123456789u;
    private uint y = 362436069u;
    private uint z = 521288629u;
    private uint w = 88675123u;

    public XorShift() { }

    public XorShift(int seed) {
        w = (uint)seed;
    }

    public uint Next() {
        t = x ^ (x << 11);
        x = y;
        y = z;
        z = w;

        w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
        return w - 1;
    }

    public uint Next(uint maxValue) {
        return Next() % maxValue;
    }

    public uint Next(uint minValue, uint maxValue) {
        return minValue + (Next() % (maxValue - minValue));
    }

    public void NextBytes(byte[] buffer) {
        for(int i = 0; i < buffer.Length; i++)
            buffer[i] = (byte)Next(256);
    }

    public double NextDouble() {
        return (double)Next() / uint.MaxValue;
    }
}

こんな感じですかね?
uintを返してしまう処が本家Randomと違う処ではありますが、メソッドの構成などは合わせてみました

ここでポイントとなるのは、Next()の最後でw - 1を返している処ですね
実は問題として指摘されているのが、XorShiftzeroを返さないよね? と…

うーん、そうなんだー
でも、zeroを返さない(かも知れない)けど、1は返す可能性があるよね?
じゃぁ、1引いちゃえばいいじゃん…
なんてね

実はNextDouble()の仕様から考えると、その方が健全かなと云う結論になります
つまり、最後に1を減ずる事でNext()の定義域が0 <= Next() < uint.MaxValue - 1となって、必然的にNextDouble()の定義域は0.0 <= NextDouble() < 1.0になる、と云う訳です
逆に云うと、Next()uint.MaxValueを返さないよね?
なんですけど、そこは目を瞑る事で7方位は丸く収まるので良いのではないでしょうか

で、実行!

XorShift rand = new XorShift();
for(int i = 0; i < 10; i++)
    Console.WriteLine(rand.Next().ToString("x08"));

みたいな感じで、こう

dca345e9
1b5116e5
951049a9
d88d00af
1ec7825d
8db24145
9af81442
2ac00f2b
0837ad57
17906568

これをベースにしてjavascriptの実装が同じ乱数列を返せば目的は達成!
と云う事で…

実装(javascript)

あ!
ここに来てなんですが、私の云っている「javascript」はwindows shellで使われる方です
IEとかChromeとかEdgeで実行できる方のjavascriptではなくて…

つまり、ちょっとばかり古いjavascriptと云う事
聞く処に因ると、新しい奴は、class定義ができたり、部分的に型の定義ができるらしい…
とか、そんな便利なんじゃない方です

勿論、スクリプトエンジンを指定すれば新しい方で動いたりする訳なんですが、起動時に1枚噛ませないといけなくなるので面倒臭いでしょ?

さて、そんな訳で取り敢えずC#javascriptに書き直してみる事にしましょうか

var XorShift = function(seed) {
    this.t = 0;
    this.x = 123456789;
    this.y = 362436069;
    this.z = 521288629;
    this.w = seed === undefined ? 88675123 : seed;

    XorShift.prototype.next = function() {
        this.t = this.x ^ (this.x << 11);
        this.x = this.y;
        this.y = this.z;
        this.z = this.w;

        this.w = (this.w ^ (this.w >>> 19)) ^ (this.t ^ (this.t >>> 8));
        return this.w - 1;
    }

    XorShift.prototype.nextDouble = function() {
        return this.next() / 0xffffffff;
    }
}

こんな感じ?
(一部のオーバロードは諸事情で割愛しました)

で、実行!

var rand = new XorShift();
for(var i=0; i<10; i++)
    WScript.Echo(rand.next().toString(16));
-235cba17
1b5116e5
-6aefb657
-2772ff51
1ec7825d
-724dbebb
-6507ebbe
2ac00f2b
837ad57
17906568

う~~~ん…
全然違う、どころかマイナスになっちゃうよ(uintであって欲しいのだが…)

考察

えーっと、何でうまくいかないか、って云うのを-

だって、javascriptuint表現できないんだもん

の一言で片づけるのは安直です

何故ならjavascriptの整数の有効桁数って53bitくらいあるんですよ?
高々32bitで表わされるビット列を11bit程度左シフトしたところで、msbまで侵食して符号反転しちゃうとは考えられません
符号は別の論理が働いて反転させられてしまっていると考えるのが合理的です

一つ実験してみましょう

> 0x7fffffff
2147483647

> 0x7fffffff + 1
2147483648

> (2147483648).toString(16)
80000000
>

ほら、所謂int.MaxValue1を足してもint.MinValueにならずにuint領域に踏み込んでしまっているでしょ?
工夫すればuintの計算だってできる可能性があるって事じゃないでしょうか?

ところで、さっきのjavascriptの実行結果、よ~く見てみるとマイナス符号が付かない奴はC#と結果が一致してますね…
でもマイナスの方は全然違う感じ

お!
そうだマイナス符号付いちゃってるんで、ビット反転してみるってのはどうでしょう?

> (0x235cba17 ^ 0xffffffff).toString(16)
-235cba18

>

あれ?
ほぼ同じだ…
あー、0xffffffffが符号反転の原因になってるのかなぁ
じゃぁ、取敢えずこう

> (0x235cba17 ^ 0x7fffffff).toString(16)
5ca345e8

>

大分近くなった(C#ではdca345e9)
乱数列の正符号側がずっーっと一致しているのと考え合わせると、ビット列は保存されている様です
今回の仮実装での表示の仕方(され方?)が問題なんでしょう

となると、マイナス表示されない様にするにはどうしたら良いか?
を探れば良いと云う事ですね

ゴールに向けて

んー
肝はやっぱり「符号(サインビット)」かと思います
そもそもjavascriptって+0-0が表現できてしまう(らしい)ので、サインビットは数値とは別に存在すると云う事ですよね

で、ここでもう一回テストです

> 123456789 << 1
246913578

> 123456789 << 2
493827156

> 123456789 << 3
987654312

> 123456789 << 4
1975308624

> 123456789 << 5
-344350048

> 123456789 << 6
-688700096

> 123456789 << 7
-1377400192

> 123456789 << 8
1540166912

> 123456789 << 9
-1214633472

> 123456789 << 10
1865700352

> 123456789 << 11
-563566592

マイナスになったりプラスになったりしますね
10進だと良く分からないので16進で表現してみましょうか

> (123456789).toString(16)
75bcd15

> (123456789 << 1).toString(16)
eb79a2a

> (123456789 << 2).toString(16)
1d6f3454

> (123456789 << 3).toString(16)
3ade68a8

> (123456789 << 4).toString(16)
75bcd150

> (123456789 << 5).toString(16)
-14865d60

4ビット左シフトまではプラスの数値ですが、5ビットシフトすると負数になってしまいます
つまり、4ビットシフトまでは32ビット数値として考えた時のmsbはゼロなので正数を表しているけど、5ビットシフトした時にmsb1が立ってしまうので負数と認識してしまうのではないでしょうか?
その結果、どこか違う所に管理されている「サインビット」が立ってしまうんじゃないのかなぁ…

と云う事で75bcd150の方を左シフトした結果から5ビットシフトの数値を考えてみましょう
ここでjavascriptの力を借りてしまうと話がややこしくなるので、手作業です

   7    5    b    c    d    1    5    0
0111 0101 1011 1100 1101 0001 0101 0000
                   <<
1110 1011 0111 1001 1010 0010 1010 0000
   e    b    7    9    a    2    a    0
                   ~
0001 0100 1000 0110 0101 1101 0101 1111
   1    4    8    6    5    d    5    f
                   +1
0001 0100 1000 0110 0101 1101 0110 0000
   1    4    8    6    5    d    6    0

シフトしたうえで2の補数をとると5ビット左シフトの結果と一致しました!
つまり、数値としてはeb79a2a0として表現されているんですが、サインビットが立っているので表示させると2の補数形式で解釈されている、と考える事が出来ます
?? あれ? 普通じゃん

………

じゃぁ、ビット列としては期待通りになっているけど、何かのタイミングで立ってしまっているどこか違うところにある「サインビット」をクリアすることができればuintの数値として表現してくれるのでは?

!!
ここで閃きました
サインなし右シフトって演算があるじゃないですか!
いや、何の論理的な根拠もないですけど、実験実験

> (123456789 << 5).toString(16)
-14865d60

> ((123456789 << 5) >>> 0).toString(16)
eb79a2a0

>

ほら、綺麗に5ビット左シフトの結果が得られました
何でこうなるのかはjavascriptの中の人に詳しい説明を聞きたい気分ですが、取敢えずサインなし0ビット右シフトを行う事でどっかにあるサインビットはクリアされるようです

ビット列演算の結果は常に保存されていた様なので、returnの値だけuint補正を掛ければ良い筈

と云う事で最終結果

var XorShift = function(seed) {
    this.t = 0;
    this.x = 123456789;
    this.y = 362436069;
    this.z = 521288629;
    this.w = seed === undefined ? 88675123 : seed;

    XorShift.prototype.next = function(min, max) {
        this.t = this.x ^ (this.x << 11);
        this.x = this.y;
        this.y = this.z;
        this.z = this.w;

        this.w = (this.w ^ (this.w >>> 19)) ^ (this.t ^ (this.t >>> 8));
        return (this.w >>> 0) - 1;
    }

    XorShift.prototype.nextDouble = function() {
        return this.next() / 0xffffffff;
    }
}

結果は、こうなりました

dca345e9
1b5116e5
951049a9
d88d00af
1ec7825d
8db24145
9af81442
2ac00f2b
837ad57
17906568

C#の結果と合いましたね

参考情報

REPLは役に立ちました

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1