隨機數(一)

黃志斌發表於2016-07-01

CoreCLR 中的 Random 類

在 .NET Framework 中,偽隨機數生成器是使用 System.Random 類來實現的。Microsoft 已經開源了 .NET Core Runtime (CoreCLR),我們可以從 GitHub 下載:

$ git clone https://github.com/dotnet/coreclr.git
$ cd coreclr
$ view src/mscorlib/src/System/Random.cs

如果不想下載 CoreCLR 的話,這個 Random 類的原始碼也可以在 ideone 中檢視。這個 Random.cs 源程式,Microsoft 寫得有點隨意了,有些不好的地方,如第 28 行:

private const int MZ = 0;

這個 MZ 以後根本就沒有用到,可以刪除。第 78 行:

Seed = 1;

也可以刪除。第 99、100 行和第 112、113 行:

int locINext = inext;
int locINextp = inextp;

inext = locINext;
inextp = locINextp;

這四行也可以刪除,第 102 至 110 行中出現的 locINext 和 locINextp 用 inext 和 inextp 代替。

第 55、56 和 98 等行的變數宣告也可以刪除,改為在後面的變數賦值時進行宣告,以符合變數的最小作用域原則。第 140 行:

bool negative = (InternalSample()%2 == 0) ? true : false;

應改為:

bool negative = InternalSample() % 2 == 0;

或者乾脆刪除 negative 這個變數,併入第 141 行的 if 語句。

上圖是在 Windows 7 作業系統中使用 ILSpy 檢視 mscorlib 中的 System.Random 類的結果。可以看出,經過 C# 編譯器編譯後,區域性變數名沒有保留下來,所以 ILSpy 就使用 num、num2、num3 等變數名。很明顯,最後一句 Seed = 1; 完全沒有必要,可以刪除。在上圖中,第 55 和 56 行的變數宣告已經刪除。

從上圖可以看出,第 140 行已經按照我前面講的進行了優化。

MsRandom 類

下面的 MsRandom 類就是把 Random 類稍做修改而得到的:

namespace Skyiv {
  using System;

  public class MsRandom
  {
    const int M = int.MaxValue;
    int[] seeds = new int[56];
    int n1 = 0, n2 = 21;

    public MsRandom() : this(Environment.TickCount) { }

    public MsRandom(int seed)
    {
      int j = 161803398-((seed==int.MinValue)?int.MaxValue:Math.Abs(seed));
      seeds[55] = j;
      for (int z, k = 1, i = 1; i < 55; i++, j = seeds[z]) {
        seeds[z = (21 * i) % 55] = k;
        if ((k = j - k) < 0) k += M;
      }
      for (int k = 1; k < 5; k++)
        for (int i = 1; i < 56; i++) {
          seeds[i] -= seeds[1 + (i + 30) % 55];
          if (seeds[i] < 0) seeds[i] += M;
        }
    }

    protected virtual double Sample() {return (InternalSample()*(1.0/M));}
    public virtual double NextDouble() { return Sample(); }
    public virtual int Next() { return InternalSample(); }

    public virtual int Next(int max)
    {
      if (max < 0) throw new ArgumentOutOfRangeException(nameof(max));
      return (int)(Sample() * max);
    }

    public virtual int Next(int min, int max)
    {
      if (min > max) throw new ArgumentOutOfRangeException(nameof(min));
      long z = (long)max - min;
      if (z <= int.MaxValue) return (int)(Sample()*z) + min;
      return (int)((long)(GetSampleForLargeRange()*z) + min);
    }

    public virtual void NextBytes(byte [] buffer)
    {
      if (buffer == null) throw new ArgumentNullException(nameof(buffer));
      for (int i = 0; i < buffer.Length; i++)
        buffer[i] = (byte)(InternalSample() % (byte.MaxValue + 1));
    }

    int InternalSample()
    {
      if (++n1 >= 56) n1 = 1;
      if (++n2 >= 56) n2 = 1;
      int z = seeds[n1] - seeds[n2];
      if (z == M) z--;
      if (z < 0) z += M;
      return seeds[n1] = z;
    }

    double GetSampleForLargeRange()
    {
      double d = InternalSample();
      if (InternalSample() % 2 == 0) d = -d;
      return (d + (int.MaxValue - 1)) / (2 * (uint)int.MaxValue- 1);
    }
  }
}

測試程式

我們使用下面的 C# 程式來測試一下:

using System;

namespace Skyiv.Test
{
  static class RandomTester
  {
    static readonly Random rand = new Random();

    static bool Test(int a, int b)
    {
      Console.Write('.');
      for (var i = 0; i < 123; i++) {
        var seed = rand.Next();
        var r1 = new Random(seed);
        var r2 = new MsRandom(seed);
        for (var j = 0; j < 987654; j++)
          if (r1.Next(a, b) != r2.Next(a, b)) {
            Console.WriteLine(" Fail.");
            return false;
          }
      }
      return true;
    }

    static void Main()
    {
      Console.WriteLine(Environment.OSVersion);
      Console.WriteLine("CLR " + Environment.Version);
      Console.Write("Test ");
      if (!Test(int.MinValue, int.MaxValue)) return;
      for (var i = 0; i < 9; i++) {
        var a = rand.Next(int.MinValue, int.MaxValue);
        if (!Test(a, rand.Next(a, int.MaxValue))) return;
      }
      Console.WriteLine(" OK.");
    }
  }
}

編譯和執行

在 Linux 作業系統中編譯和執行:

$ mcs RandomTester.cs MsRandom.cs
$ mono RandomTester.exe
Unix 4.6.3.1
CLR 4.0.30319.42000
Test .......... OK.

在 Windows 作業系統中執行:

Microsoft Windows [版本 6.1.7601]
版權所有 (c) 2009 Microsoft Corporation。保留所有權利。
C> RandomTester.exe
Microsoft Windows NT 6.1.7601 Service Pack 1
CLR 4.0.30319.42000
Test .......... OK.

來源

實際上,這個 Random 程式來自《C語言數值演算法程式大全(第二版)》第七章 隨機數

7.1.2 可移植的隨機數生成程式

最後,我們把 Knuth 建議[4]的可移植程式提供給大家,我們已將它轉換成為現在的程式,稱作 ran3。這個 ran3 根本不是基於線性同餘的方法,而是以一個相減方法為基礎(見[5])。

Knuth, D.E. 1981. Seminumerical Algorithms, 2nded, vol. 2 of The Art of Computer Programming (Raeding, MA: Addison-Wesley), §§3.2~3.3. [4]
Kahaner, D., Moler, C, and Nash, S. 1989, Numerical Methods and Software (Englewood Cliffs, NJ: Prentice Hall), Chapter 10. [5]

這個 ran3 是 C 語言程式,Microsoft 把它改寫為 C# 語言程式。

此外,這本書還告誡我們不要使用系統提供的隨機數生成器:

7.1.1 系統提供的隨機數生成程式

多數 c 語言內部蘊含有可以初始化並能生成“隨機數”的 ANSI 庫函式。典型程式為:
......
在本章中,這一節最重要的內容是,對系統提供的 rand() 產生很大很大的懷疑,因為它同我們剛剛敘述的內容很類似。由於不合理的函式 rand() 使一些科學論文的結果值得懷疑,如果將這些科學論文從圖書館的書架上全部取走的話,那麼在每個書架上就會留出一拳之寬空隙。

參考資料

  1. MSDN: Random Class (System)
  2. GitHub: dotnet/coreclr


相關文章