diff --git a/FastRng/MultiThreadedRng.cs b/FastRng/MultiThreadedRng.cs index cf9c6b8..0e41672 100644 --- a/FastRng/MultiThreadedRng.cs +++ b/FastRng/MultiThreadedRng.cs @@ -1,12 +1,17 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; +using FastRng.Distributions; namespace FastRng { + /// + /// This class uses the George Marsaglia's MWC algorithm. The algorithm's implementation based loosely on John D. + /// Cook's (johndcook.com) implementation (https://www.codeproject.com/Articles/25172/Simple-Random-Number-Generation). + /// Thanks John for your work. + /// public sealed class MultiThreadedRng : IRandom { #if DEBUG @@ -15,15 +20,23 @@ namespace FastRng private const int CAPACITY_RANDOM_NUMBERS_4_SOURCE = 16_000_000; #endif - private static readonly object SYNC = new object(); - - private readonly System.Random rng = new System.Random(); private readonly CancellationTokenSource producerToken = new CancellationTokenSource(); + private readonly object syncUintGenerators = new object(); + private readonly object syncUniformDistributedDoubleGenerators = new object(); + private readonly Thread[] producerRandomUint = new Thread[2]; + private readonly Thread[] producerRandomUniformDistributedDouble = new Thread[2]; - private readonly Thread producerRandom1; - private readonly Thread producerRandom2; + private uint mW; + private uint mZ; - private readonly Channel channelRandom = Channel.CreateBounded(new BoundedChannelOptions(CAPACITY_RANDOM_NUMBERS_4_SOURCE) + private readonly Channel channelRandomUint = Channel.CreateBounded(new BoundedChannelOptions(CAPACITY_RANDOM_NUMBERS_4_SOURCE) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = false, + SingleWriter = false, + }); + + private readonly Channel channelRandomUniformDistributedDouble = Channel.CreateBounded(new BoundedChannelOptions(CAPACITY_RANDOM_NUMBERS_4_SOURCE) { FullMode = BoundedChannelFullMode.Wait, SingleReader = false, @@ -34,61 +47,61 @@ namespace FastRng public MultiThreadedRng() { - this.producerRandom1 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; - this.producerRandom2 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; - this.producerRandom1.Start(); - this.producerRandom2.Start(); + // + // Initialize the mW and mZ by using + // the system's time. + // + var now = DateTime.Now; + var ticks = now.Ticks; + this.mW = (uint) (ticks >> 16); + this.mZ = (uint) (ticks % 4294967296); + this.StartProducerThreads(); } - public MultiThreadedRng(int seed) + public MultiThreadedRng(uint seedU) { - this.rng = new Random(seed); + this.mW = seedU; + this.mZ = 362436069; + this.StartProducerThreads(); + } + + public MultiThreadedRng(uint seedU, uint seedV) + { + this.mW = seedU; + this.mZ = seedV; + this.StartProducerThreads(); + } + + private void StartProducerThreads() + { + this.producerRandomUint[0] = new Thread(() => this.RandomProducerUint(this.channelRandomUint.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandomUint[1] = new Thread(() => this.RandomProducerUint(this.channelRandomUint.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandomUint[0].Start(); + this.producerRandomUint[1].Start(); - this.producerRandom1 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; - this.producerRandom2 = new Thread(() => MultiThreadedRng.RandomProducer(this.rng, this.channelRandom.Writer, this.producerToken.Token)) {IsBackground = true}; - - this.producerRandom1.Start(); - this.producerRandom2.Start(); + this.producerRandomUniformDistributedDouble[0] = new Thread(() => this.RandomProducerUniformDistributedDouble(this.channelRandomUint.Reader, channelRandomUniformDistributedDouble.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandomUniformDistributedDouble[1] = new Thread(() => this.RandomProducerUniformDistributedDouble(this.channelRandomUint.Reader, channelRandomUniformDistributedDouble.Writer, this.producerToken.Token)) {IsBackground = true}; + this.producerRandomUniformDistributedDouble[0].Start(); + this.producerRandomUniformDistributedDouble[1].Start(); } #endregion - + + #region Producers + [ExcludeFromCodeCoverage] - private static async void RandomProducer(System.Random random, ChannelWriter channelWriter, CancellationToken cancellationToken) + private async void RandomProducerUint(ChannelWriter channelWriter, CancellationToken cancellationToken) { + var buffer = new uint[CAPACITY_RANDOM_NUMBERS_4_SOURCE]; while (!cancellationToken.IsCancellationRequested) { - // - // We using double as basis for anything. That's what .NET does internally as well, cf. https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.CoreLib/src/System/Random.cs. - // random.NextDouble() returns Sample(). Next(min, max) uses GetSampleForLargeRange(). - // Thus, we re-implement GetSampleForLargeRange() and use its numbers as source for everything. - // - - var buffer = new double[CAPACITY_RANDOM_NUMBERS_4_SOURCE]; - - // - // Random is not thread-safe! - // Because we using two threads, we ensure that one threads generates - // next bag of numbers while the other pumps its numbers into the channel. - // - lock (SYNC) + lock (syncUintGenerators) { for (var n = 0; n < buffer.Length && !cancellationToken.IsCancellationRequested; n++) { - #region Re-implementation of GetSampleForLargeRange() method of .NET - - var result = random.Next(); // Notice: random.Next() is identical to InternalSample() - var negative = random.Next() % 2 == 0; // Notice: random.Next() is identical to InternalSample() - if (negative) - result = -result; - - double d = result; - d += (int.MaxValue - 1); // get a number in range [0 .. 2 * Int32MaxValue - 1) - d /= 2 * (uint)int.MaxValue - 1; - - #endregion - - buffer[n] = d; + this.mZ = 36_969 * (this.mZ & 65_535) + (this.mZ >> 16); + this.mW = 18_000 * (this.mW & 65_535) + (this.mW >> 16); + buffer[n] = (this.mZ << 16) + this.mW; } } @@ -96,25 +109,78 @@ namespace FastRng await channelWriter.WriteAsync(buffer[n], cancellationToken); } } + + [ExcludeFromCodeCoverage] + private async void RandomProducerUniformDistributedDouble(ChannelReader channelReaderUint, ChannelWriter channelWriter, CancellationToken cancellationToken) + { + var buffer = new double[CAPACITY_RANDOM_NUMBERS_4_SOURCE]; + var randomUint = new uint[CAPACITY_RANDOM_NUMBERS_4_SOURCE]; + while (!cancellationToken.IsCancellationRequested) + { + for (var n = 0; n < randomUint.Length; n++) + randomUint[n] = await channelReaderUint.ReadAsync(cancellationToken); + + lock (syncUniformDistributedDoubleGenerators) + for (var n = 0; n < buffer.Length && !cancellationToken.IsCancellationRequested; n++) + buffer[n] = (randomUint[n] + 1.0) * 2.328306435454494e-10; // 2.328 => 1/(2^32 + 2) + + for (var n = 0; n < buffer.Length && !cancellationToken.IsCancellationRequested; n++) + await channelWriter.WriteAsync(buffer[n], cancellationToken); + } + } + + #endregion #region Implementing interface - public async Task NextNumber(uint rangeStart, uint rangeEnd, CancellationToken cancel = default(CancellationToken)) + public async Task GetUniformDouble(CancellationToken cancel = default) => await this.channelRandomUniformDistributedDouble.Reader.ReadAsync(cancel); + + public async Task NextNumber(uint rangeStart, uint rangeEnd, IDistribution distribution, CancellationToken cancel = default) { + if (rangeStart > rangeEnd) + { + var tmp = rangeStart; + rangeStart = rangeEnd; + rangeEnd = tmp; + } + var range = rangeEnd - rangeStart; - return (uint) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + distribution.Random = this; + + var distributedValue = await distribution.GetDistributedValue(cancel); + return (uint) ((distributedValue * range) + rangeStart); } - public async Task NextNumber(ulong rangeStart, ulong rangeEnd, CancellationToken cancel = default(CancellationToken)) + public async Task NextNumber(ulong rangeStart, ulong rangeEnd, IDistribution distribution, CancellationToken cancel = default(CancellationToken)) { + if (rangeStart > rangeEnd) + { + var tmp = rangeStart; + rangeStart = rangeEnd; + rangeEnd = tmp; + } + var range = rangeEnd - rangeStart; - return (ulong) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + distribution.Random = this; + + var distributedValue = await distribution.GetDistributedValue(cancel); + return (ulong) ((distributedValue * range) + rangeStart); } - public async Task NextNumber(float rangeStart, float rangeEnd, CancellationToken cancel = default(CancellationToken)) + public async Task NextNumber(float rangeStart, float rangeEnd, IDistribution distribution, CancellationToken cancel = default(CancellationToken)) { + if (rangeStart > rangeEnd) + { + var tmp = rangeStart; + rangeStart = rangeEnd; + rangeEnd = tmp; + } + var range = rangeEnd - rangeStart; - return (float) ((await this.channelRandom.Reader.ReadAsync(cancel) * range) + rangeStart); + distribution.Random = this; + + var distributedValue = await distribution.GetDistributedValue(cancel); + return (float) ((distributedValue * range) + rangeStart); } public void StopProducer() => this.producerToken.Cancel(); diff --git a/FastRngTests/MultiThreadedRngTests.cs b/FastRngTests/MultiThreadedRngTests.cs index 082219c..94e66e4 100644 --- a/FastRngTests/MultiThreadedRngTests.cs +++ b/FastRngTests/MultiThreadedRngTests.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using FastRng; +using FastRng.Distributions; using NUnit.Framework; namespace FastRngTests @@ -17,8 +18,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange01Uint() { + var dist = new Uniform(); for (uint n = 0; n < 1_000_000; n++) - Assert.That(await rng.NextNumber(n, 100_000 + n), Is.InRange(n, 100_000 + n)); + Assert.That(await rng.NextNumber(n, 100_000 + n, dist), Is.InRange(n, 100_000 + n)); } [Test] @@ -26,8 +28,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange01Ulong() { + var dist = new Uniform(); for (ulong n = 0; n < 1_000_000; n++) - Assert.That(await rng.NextNumber(n, 100_000 + n), Is.InRange(n, 100_000 + n)); + Assert.That(await rng.NextNumber(n, 100_000 + n, dist), Is.InRange(n, 100_000 + n)); } [Test] @@ -35,8 +38,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange01Float() { + var dist = new Uniform(); for (var n = 0f; n < 1e6f; n++) - Assert.That(await rng.NextNumber(n, 100_000 + n), Is.InRange(n, 100_000 + n)); + Assert.That(await rng.NextNumber(n, 100_000 + n, dist), Is.InRange(n, 100_000 + n)); } [Test] @@ -44,9 +48,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange02Uint() { - Assert.That(await rng.NextNumber(5, 5), Is.EqualTo(5)); - Assert.That(await rng.NextNumber(0, 0), Is.EqualTo(0)); - Assert.That(await rng.NextNumber(3_000_000_000, 3_000_000_000), Is.EqualTo(3_000_000_000)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5, 5, dist), Is.EqualTo(5)); + Assert.That(await rng.NextNumber(0, 0, dist), Is.EqualTo(0)); + Assert.That(await rng.NextNumber(3_000_000_000, 3_000_000_000, dist), Is.EqualTo(3_000_000_000)); } [Test] @@ -54,9 +59,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange02Ulong() { - Assert.That(await rng.NextNumber(5UL, 5), Is.EqualTo(5)); - Assert.That(await rng.NextNumber(0UL, 0), Is.EqualTo(0)); - Assert.That(await rng.NextNumber(3_000_000_000UL, 3_000_000_000), Is.EqualTo(3_000_000_000)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5UL, 5, dist), Is.EqualTo(5)); + Assert.That(await rng.NextNumber(0UL, 0, dist), Is.EqualTo(0)); + Assert.That(await rng.NextNumber(3_000_000_000UL, 3_000_000_000, dist), Is.EqualTo(3_000_000_000)); } [Test] @@ -64,9 +70,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange02Float() { - Assert.That(await rng.NextNumber(5f, 5f), Is.EqualTo(5)); - Assert.That(await rng.NextNumber(0f, 0f), Is.EqualTo(0)); - Assert.That(await rng.NextNumber(3e9f, 3e9f), Is.EqualTo(3e9f)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5f, 5f, dist), Is.EqualTo(5)); + Assert.That(await rng.NextNumber(0f, 0f, dist), Is.EqualTo(0)); + Assert.That(await rng.NextNumber(3e9f, 3e9f, dist), Is.EqualTo(3e9f)); } [Test] @@ -74,9 +81,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange03Uint() { - Assert.That(await rng.NextNumber(5, 6), Is.InRange(5, 6)); - Assert.That(await rng.NextNumber(0, 1), Is.InRange(0, 1)); - Assert.That(await rng.NextNumber(3_000_000_000, 3_000_000_002), Is.InRange(3_000_000_000, 3_000_000_002)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5, 6, dist), Is.InRange(5, 6)); + Assert.That(await rng.NextNumber(0, 1, dist), Is.InRange(0, 1)); + Assert.That(await rng.NextNumber(3_000_000_000, 3_000_000_002, dist), Is.InRange(3_000_000_000, 3_000_000_002)); } [Test] @@ -84,9 +92,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange03Ulong() { - Assert.That(await rng.NextNumber(5UL, 6), Is.InRange(5, 6)); - Assert.That(await rng.NextNumber(0UL, 1), Is.InRange(0, 1)); - Assert.That(await rng.NextNumber(3_000_000_000UL, 3_000_000_002), Is.InRange(3_000_000_000, 3_000_000_002)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5UL, 6, dist), Is.InRange(5, 6)); + Assert.That(await rng.NextNumber(0UL, 1, dist), Is.InRange(0, 1)); + Assert.That(await rng.NextNumber(3_000_000_000UL, 3_000_000_002, dist), Is.InRange(3_000_000_000, 3_000_000_002)); } [Test] @@ -94,9 +103,10 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange03Float() { - Assert.That(await rng.NextNumber(5f, 6), Is.InRange(5, 6)); - Assert.That(await rng.NextNumber(0f, 1), Is.InRange(0, 1)); - Assert.That(await rng.NextNumber(3e9f, 3e9f+2), Is.InRange(3e9f, 3e9f+2)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(5f, 6, dist), Is.InRange(5, 6)); + Assert.That(await rng.NextNumber(0f, 1, dist), Is.InRange(0, 1)); + Assert.That(await rng.NextNumber(3e9f, 3e9f+2, dist), Is.InRange(3e9f, 3e9f+2)); } [Test] @@ -104,8 +114,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange04Uint() { - Assert.That(await rng.NextNumber(10, 1), Is.InRange(1, 10)); - Assert.That(await rng.NextNumber(20, 1), Is.InRange(1, 20)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(10, 1, dist), Is.InRange(1, 10)); + Assert.That(await rng.NextNumber(20, 1, dist), Is.InRange(1, 20)); } [Test] @@ -113,8 +124,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange04Ulong() { - Assert.That(await rng.NextNumber(10UL, 1), Is.InRange(1, 10)); - Assert.That(await rng.NextNumber(20UL, 1), Is.InRange(1, 20)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(10UL, 1, dist), Is.InRange(1, 10)); + Assert.That(await rng.NextNumber(20UL, 1, dist), Is.InRange(1, 20)); } [Test] @@ -122,8 +134,9 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange04Float() { - Assert.That(await rng.NextNumber(10f, 1), Is.InRange(1, 10)); - Assert.That(await rng.NextNumber(20f, 1), Is.InRange(1, 20)); + var dist = new Uniform(); + Assert.That(await rng.NextNumber(10f, 1, dist), Is.InRange(1, 10)); + Assert.That(await rng.NextNumber(20f, 1, dist), Is.InRange(1, 20)); } [Test] @@ -131,10 +144,11 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange05Uint() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0, 100)]++; + distribution[await rng.NextNumber(0, 100, dist)]++; for (var n = 0; n < distribution.Length - 1; n++) Assert.That(distribution[n], Is.GreaterThan(0)); @@ -145,10 +159,11 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange05Ulong() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0UL, 100)]++; + distribution[await rng.NextNumber(0UL, 100, dist)]++; for (var n = 0; n < distribution.Length - 1; n++) Assert.That(distribution[n], Is.GreaterThan(0)); @@ -159,10 +174,11 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestRange05Float() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f))]++; + distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f, dist))]++; for (var n = 0; n < distribution.Length - 1; n++) Assert.That(distribution[n], Is.GreaterThan(0)); @@ -172,72 +188,78 @@ namespace FastRngTests [Category(TestCategories.NORMAL)] public async Task TestDistribution001Uint() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0, 100)]++; + distribution[await rng.NextNumber(0, 100, dist)]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 600)); } [Test] [Category(TestCategories.NORMAL)] public async Task TestDistribution001Ulong() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0UL, 100)]++; + distribution[await rng.NextNumber(0UL, 100, dist)]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 600)); } [Test] [Category(TestCategories.NORMAL)] public async Task TestDistribution001Float() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 1_000_000; for (var n = 0; n < runs; n++) - distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f))]++; + distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f, dist))]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 600)); } [Test] [Category(TestCategories.LONG_RUNNING)] public async Task TestDistribution002Uint() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 100_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0, 100)]++; + distribution[await rng.NextNumber(0, 100, dist)]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 6_000)); } [Test] [Category(TestCategories.LONG_RUNNING)] public async Task TestDistribution002Ulong() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 100_000_000; for (var n = 0; n < runs; n++) - distribution[await rng.NextNumber(0UL, 100)]++; + distribution[await rng.NextNumber(0UL, 100, dist)]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 6_000)); } [Test] [Category(TestCategories.LONG_RUNNING)] public async Task TestDistribution002Float() { + var dist = new Uniform(); var distribution = new uint[101]; var runs = 100_000_000; for (var n = 0; n < runs; n++) - distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f))]++; + distribution[(uint)MathF.Floor(await rng.NextNumber(0f, 100f, dist))]++; - Assert.That(distribution[..100].Max() - distribution[..100].Min(), Is.InRange(0, 600)); + Assert.That(distribution[..^1].Max() - distribution[..^1].Min(), Is.InRange(0, 6_000)); } } } \ No newline at end of file