Unity.Mathematics.Random の特性と CreateFromIndex()

インターネットをご覧の皆さん こんばんは。
Unity.Mathematics.Random を使用した際に学んだ特性をメモしておこうかと思います。

試した時点の環境は

- com.unity.mathematics 1.3.1

です。

# seed を変えてもランダムにならない？
Unity.Mathematics.Random を下記のように使用していました。
```csharp
var seed = 123u;
var rand = new Unity.Mathematics.Random(seed);  // seed で初期化

var max = 10;
var result = rand.NextInt(max);  // [0, max) の範囲で結果を取得
```
この時、seed の値を変更しても結果がランダムになりませんでした。

下記は seed の値を変えてみた結果です。
```csharp:テストコード
void Rand(uint seed_from, uint seed_to, int max)
{
  var first_results = new Dictionary<int, int>();
  var second_results = new Dictionary<int, int>();
  for (var i = 0; i < max; i++) {
      first_results[i] = 0;
      second_results[i] = 0;
  }

  for (uint seed = seed_from; seed <= seed_to; seed++) {
    var rand = new Unity.Mathematics.Random(seed);
    first_results[rand.NextInt(max)]++;
    second_results[rand.NextInt(max)]++;
  }

  Debug.Log($"[seed={seed_from}-{seed_to}] NextInt({max})");
  Debug.Log("1st: " + string.Join(", ", first_results.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}")));
  Debug.Log("2nd: " + string.Join(", ", second_results.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}")));
}
Rand(    1,  1000,  10);
Rand( 5001,  6000,  10);
Rand(10001, 11000,  10);
```
```shell:結果
> [seed=1-1000] NextInt(10)
> 1st: 0=1000, 1=0, 2=0, 3=0, 4=0, 5=0, 6=0, 7=0, 8=0, 9=0
> 2nd: 0=103, 1=104, 2=104, 3=96, 4=104, 5=104, 6=99, 7=91, 8=98, 9=97
> [seed=5001-6000] NextInt(10)
> 1st: 0=0, 1=0, 2=42, 3=958, 4=0, 5=0, 6=0, 7=0, 8=0, 9=0
> 2nd: 0=104, 1=96, 2=101, 3=98, 4=98, 5=97, 6=103, 7=95, 8=104, 9=104
> [seed=10001-11000] NextInt(10)
> 1st: 0=0, 1=0, 2=0, 3=0, 4=0, 5=85, 6=915, 7=0, 8=0, 9=0
> 2nd: 0=97, 1=98, 2=95, 3=104, 4=101, 5=102, 6=94, 7=101, 8=104, 9=104
```
初回の結果に偏りがあることがわかりました。

# Unity.Mathematics.Random の特性
Unity.Mathematics.Random は xorshift をベースに実装されているようです。

https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/random.cs

```csharp:抜粋
public Random(uint seed)
{
  state = seed;
  CheckInitState();
  NextState();
}

private uint NextState()
{
  CheckState();
  uint t = state;
  state ^= state << 13;
  state ^= state >> 17;
  state ^= state << 5;
  return t;
}

public int NextInt(int max)
{
  CheckNextIntMax(max);
  return (int)((NextState() * (ulong)max) >> 32);
}
```

seed の値が近いと上位バイトが同じになるため、初回のランダム値が偏ってしまうようです。
```csharp:Random.state
var first_results = new List<uint>();
var second_results = new List<uint>();
for (uint seed = 1; seed <= 10; seed++)
{
  var rand = new Unity.Mathematics.Random(seed);
  first_results.Add(rand.state);
  rand.NextInt();
  second_results.Add(rand.state);
}
Debug.Log($"1st: {string.Join(", ", first_results.Select(x => $"{x:X8}"))}");
Debug.Log($"2nd: {string.Join(", ", second_results.Select(x => $"{x:X8}"))}");
```
```shell
1st: 00042021, 00084042, 000C6063, 00108084, 0014A0A5, 0018C0C6, 001CE0E7, 00210108, 00252129, 0029414A
2nd: 04080601, 08008C02, 0C088A03, 10011804, 14091E05, 18019406, 1C099207, 20023008, 240A3609, 2802BC0A
```

# Unity.Mathematics.Random.CreateFromIndex()
調べてみると、Unity.Mathematics.Random には CreateFromIndex(uint index) というメソッドが存在することがわかりました。

https://docs.unity3d.com/Packages/com.unity.mathematics@1.3/api/Unity.Mathematics.Random.CreateFromIndex.html

CreateFromIndex(uint index) は、渡された index をもとに Hash を計算して seed としてくれるようです。

https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/random.cs#L57

Random.CreateFromIndex() で生成したものは、初回の結果からランダムになりました。
```shell:テストコードの生成個所をRandom.CreateFromIndexにした結果
[seed=1-1000] NextInt(10)
1st: 0=97, 1=112, 2=99, 3=100, 4=115, 5=112, 6=89, 7=104, 8=90, 9=82
2nd: 0=80, 1=100, 2=95, 3=101, 4=103, 5=117, 6=116, 7=92, 8=100, 9=96
[seed=5001-6000] NextInt(10)
1st: 0=88, 1=105, 2=100, 3=124, 4=85, 5=96, 6=103, 7=100, 8=87, 9=112
2nd: 0=108, 1=87, 2=93, 3=102, 4=98, 5=118, 6=100, 7=102, 8=92, 9=100
[seed=10001-11000] NextInt(10)
1st: 0=91, 1=88, 2=95, 3=104, 4=129, 5=108, 6=108, 7=107, 8=90, 9=80
2nd: 0=91, 1=107, 2=96, 3=84, 4=98, 5=101, 6=109, 7=106, 8=97, 9=111
```

# おわりに
今回は DateTime.Now.Millisecond を seed として使用してしまったため遭遇した現象でした。見知った処理でも知らないことはあるものだと気を付けようと思いました。

以上 おつきあいいただきありがとうございました。