diff --git a/FastRng/IRandom.cs b/FastRng/IRandom.cs index a455859..6a250ba 100644 --- a/FastRng/IRandom.cs +++ b/FastRng/IRandom.cs @@ -18,4 +18,29 @@ public interface IRandom : IDisposable where TNum : IFloatingPointIeee754< /// by using multiple threads at the same time. /// public TNum GetUniform(CancellationToken cancel = default); + + /// + /// Get a uniform distributed pseudo-random number from the interval [0, max). + /// + /// + /// This method is thread-safe. You can consume numbers from the same generator + /// by using multiple threads at the same time. + /// + /// The maximum value (exclusive). The max value returned will be max - 1. + /// The cancellation token. + /// A pseudo-random number from the interval [0, max). + public int GetUniformInt(int max, CancellationToken cancel = default); + + /// + /// Get a uniform distributed pseudo-random number from the interval [min, max). + /// + /// + /// This method is thread-safe. You can consume numbers from the same generator + /// by using multiple threads at the same time. + /// + /// The minimum value (inclusive). + /// The maximum value (exclusive). The max value returned will be max - 1. + /// The cancellation token. + /// A pseudo-random number from the interval [min, max). + public int GetUniformInt(int min, int max, CancellationToken cancel = default); } \ No newline at end of file diff --git a/FastRng/MultiChannelRng.cs b/FastRng/MultiChannelRng.cs index d3d9abd..ff11f7f 100644 --- a/FastRng/MultiChannelRng.cs +++ b/FastRng/MultiChannelRng.cs @@ -165,6 +165,20 @@ public sealed class MultiChannelRng : IRandom, IDisposable where TNu return valueTask.AsTask().Result; } + /// + public int GetUniformInt(int max, CancellationToken cancel = default) + { + var valueTask = this.channelIntegers.Reader.ReadAsync(cancel); + return (int) (valueTask.AsTask().Result % (uint) max); + } + + /// + public int GetUniformInt(int min, int max, CancellationToken cancel = default) + { + var valueTask = this.channelIntegers.Reader.ReadAsync(cancel); + return (int) (valueTask.AsTask().Result % (uint) (max - min) + (uint) min); + } + #endregion #endregion diff --git a/FastRng/MultiThreadedRng.cs b/FastRng/MultiThreadedRng.cs index e7fbfba..bfeb0b2 100644 --- a/FastRng/MultiThreadedRng.cs +++ b/FastRng/MultiThreadedRng.cs @@ -61,6 +61,8 @@ public sealed class MultiThreadedRng : IRandom, IDisposable where TN // The uniform float producer thread: private Thread producerRandomUniformDistributedFloat; + + private readonly UIntChannelProducer independentUIntProducer = new(); // Variable w and z for the uint generator. Both get used // as seeding variable as well (cf. constructors) @@ -312,6 +314,20 @@ public sealed class MultiThreadedRng : IRandom, IDisposable where TN // return this.currentBuffer[myPointer]; } + + /// + public int GetUniformInt(int max, CancellationToken cancel = default) + { + var valueTask = this.independentUIntProducer.GetNextAsync(cancel); + return (int) (valueTask.AsTask().Result % (uint) max); + } + + /// + public int GetUniformInt(int min, int max, CancellationToken cancel = default) + { + var valueTask = this.independentUIntProducer.GetNextAsync(cancel); + return (int) (valueTask.AsTask().Result % (uint) (max - min) + (uint) min); + } private void StopProducer() => this.producerTokenSource.Cancel(); @@ -320,7 +336,11 @@ public sealed class MultiThreadedRng : IRandom, IDisposable where TN /// when it is no longer needed. Otherwise, the background threads /// are still running. /// - public void Dispose() => this.StopProducer(); + public void Dispose() + { + this.independentUIntProducer.Dispose(); + this.StopProducer(); + } #endregion } \ No newline at end of file diff --git a/FastRng/UIntChannelProducer.cs b/FastRng/UIntChannelProducer.cs new file mode 100644 index 0000000..f4121d2 --- /dev/null +++ b/FastRng/UIntChannelProducer.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace FastRng; + +internal sealed class UIntChannelProducer : IDisposable +{ +#if DEBUG + private const int BUFFER_SIZE = 1_000_000; +#else + private const int BUFFER_SIZE = 1_000_000; +#endif + + private readonly Channel channelIntegers = Channel.CreateBounded(new BoundedChannelOptions(capacity: BUFFER_SIZE * 2) { FullMode = BoundedChannelFullMode.Wait, SingleWriter = true, SingleReader = true }); + + // Gets used to stop the producer thread: + private readonly CancellationTokenSource producerTokenSource = new(); + + // The uint producer thread: + private Thread producerRandomUint; + + // Variable w and z for the uint generator. Both get used + // as seeding variable as well (cf. constructors) + private uint mW; + private uint mZ; + + #region Constructors + + public UIntChannelProducer() + { + // + // 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 % 4_294_967_296); + this.StartProducerThread(); + } + + /// + /// Creates a multithreaded random number generator. + /// + /// + /// A multi-threaded random number generator created by this constructor is + /// deterministic. It's behaviour is not depending on the time of its creation.

+ /// + /// Please note: Although the number generator and all distributions are deterministic, + /// the behavior of the consuming application might be non-deterministic. This is possible if + /// the application with multiple threads consumes the numbers. The scheduling of the threads + /// is up to the operating system and might not be predictable. + ///
+ /// A seed value to generate a deterministic generator. + public UIntChannelProducer(uint seedU) + { + this.mW = seedU; + this.mZ = 362_436_069; + this.StartProducerThread(); + } + + /// + /// Creates a multi-threaded random number generator. + /// + /// + /// A multi-threaded random number generator created by this constructor is + /// deterministic. It's behaviour is not depending on the time of its creation.

+ /// + /// Please note: Although the number generator and all distributions are deterministic, + /// the behavior of the consuming application might be non-deterministic. This is possible if + /// the application with multiple threads consumes the numbers. The scheduling of the threads + /// is up to the operating system and might not be predictable. + ///
+ /// The first seed value. + /// The second seed value. + public UIntChannelProducer(uint seedU, uint seedV) + { + this.mW = seedU; + this.mZ = seedV; + this.StartProducerThread(); + } + + private void StartProducerThread() + { + this.producerRandomUint = new Thread(() => this.RandomProducerUint(this.producerTokenSource.Token)) {IsBackground = true}; + this.producerRandomUint.Start(); + } + + #endregion + + [ExcludeFromCodeCoverage] + private async void RandomProducerUint(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + this.mZ = 36_969 * (this.mZ & 65_535) + (this.mZ >> 16); + this.mW = 18_000 * (this.mW & 65_535) + (this.mW >> 16); + await this.channelIntegers.Writer.WriteAsync((this.mZ << 16) + this.mW, cancellationToken); + } + } + catch (OperationCanceledException) + { + } + } + + public async ValueTask GetNextAsync(CancellationToken cancellationToken = default) => await this.channelIntegers.Reader.ReadAsync(cancellationToken); + + #region Implementation of IDisposable + + private void StopProducer() => this.producerTokenSource.Cancel(); + + /// + public void Dispose() => this.StopProducer(); + + #endregion +} \ No newline at end of file diff --git a/FastRngTests/GetIntTests.cs b/FastRngTests/GetIntTests.cs new file mode 100644 index 0000000..8864fa4 --- /dev/null +++ b/FastRngTests/GetIntTests.cs @@ -0,0 +1,327 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; + +using FastRng; +using FastRng.Distributions; + +using NUnit.Framework; + +namespace FastRngTests; + +[ExcludeFromCodeCoverage] +public class GetIntTests +{ + #region Channel-Based + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelGetMax() + { + var random = new Random(); + var randomMax = random.Next(); + + using var rng = new MultiChannelRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMax); + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelGetMaxDistributionCheck() + { + var random = new Random(); + var randomMax = random.Next(3, 33); + var freqCheck = new IntFrequencyAnalysis(randomMax); + using var rng = new MultiChannelRng(); + + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMax); + freqCheck.CountThis(value); + + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + + var distribution = freqCheck.NormalizeAndPlotEvents(TestContext.WriteLine); + var max = distribution.Max(); + var min = distribution.Min(); + Assert.That(max - min, Is.LessThanOrEqualTo(0.27f)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelGetMinMax() + { + var random = new Random(); + var randomMin = random.Next(); + int randomMax; + do + { + randomMax = random.Next(); + } while (randomMax < randomMin); + + using var rng = new MultiChannelRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMin, randomMax); + Assert.That(value, Is.GreaterThanOrEqualTo(randomMin)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelGetMinMaxDistributionCheck() + { + var random = new Random(); + var randomMin = random.Next(0, 16); + var randomMax = random.Next(randomMin + 10, 33); + + var freqCheck = new IntFrequencyAnalysis(randomMax - randomMin); + using var rng = new MultiChannelRng(); + + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMin, randomMax); + freqCheck.CountThis(value - randomMin); + + Assert.That(value, Is.GreaterThanOrEqualTo(randomMin)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + + var distribution = freqCheck.NormalizeAndPlotEvents(TestContext.WriteLine); + var max = distribution.Max(); + var min = distribution.Min(); + Assert.That(max - min, Is.LessThanOrEqualTo(0.27f)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelCheckMinMax01() + { + var expectedMax = 3; + + var max = int.MinValue; + var min = int.MaxValue; + using var rng = new MultiChannelRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(expectedMax); + if (value < min) + min = value; + + if (value > max) + max = value; + + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(expectedMax)); + } + + Assert.That(min, Is.EqualTo(0)); + Assert.That(max, Is.EqualTo(expectedMax - 1)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void ChannelCheckMinMax02() + { + var expectedMin = 2; + var expectedMax = 6; + + var max = int.MinValue; + var min = int.MaxValue; + using var rng = new MultiChannelRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(expectedMin, expectedMax); + if (value < min) + min = value; + + if (value > max) + max = value; + + Assert.That(value, Is.GreaterThanOrEqualTo(expectedMin)); + Assert.That(value, Is.LessThanOrEqualTo(expectedMax)); + } + + Assert.That(min, Is.EqualTo(expectedMin)); + Assert.That(max, Is.EqualTo(expectedMax - 1)); + } + + #endregion + + #region Multithreaded + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedGetMax() + { + var random = new Random(); + var randomMax = random.Next(); + + using var rng = new MultiThreadedRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMax); + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedGetMaxDistributionCheck() + { + var random = new Random(); + var randomMax = random.Next(3, 33); + var freqCheck = new IntFrequencyAnalysis(randomMax); + using var rng = new MultiThreadedRng(); + + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMax); + freqCheck.CountThis(value); + + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + + var distribution = freqCheck.NormalizeAndPlotEvents(TestContext.WriteLine); + var max = distribution.Max(); + var min = distribution.Min(); + Assert.That(max - min, Is.LessThanOrEqualTo(0.27f)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedGetMinMax() + { + var random = new Random(); + var randomMin = random.Next(); + int randomMax; + do + { + randomMax = random.Next(); + } while (randomMax < randomMin); + + using var rng = new MultiThreadedRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMin, randomMax); + Assert.That(value, Is.GreaterThanOrEqualTo(randomMin)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedGetMinMaxDistributionCheck() + { + var random = new Random(); + var randomMin = random.Next(0, 16); + var randomMax = random.Next(randomMin + 10, 33); + + var freqCheck = new IntFrequencyAnalysis(randomMax - randomMin); + using var rng = new MultiThreadedRng(); + + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(randomMin, randomMax); + freqCheck.CountThis(value - randomMin); + + Assert.That(value, Is.GreaterThanOrEqualTo(randomMin)); + Assert.That(value, Is.LessThanOrEqualTo(randomMax)); + } + + var distribution = freqCheck.NormalizeAndPlotEvents(TestContext.WriteLine); + var max = distribution.Max(); + var min = distribution.Min(); + Assert.That(max - min, Is.LessThanOrEqualTo(0.27f)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedCheckMinMax01() + { + var expectedMax = 3; + + var max = int.MinValue; + var min = int.MaxValue; + using var rng = new MultiThreadedRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(expectedMax); + if (value < min) + min = value; + + if (value > max) + max = value; + + Assert.That(value, Is.GreaterThanOrEqualTo(0)); + Assert.That(value, Is.LessThanOrEqualTo(expectedMax)); + } + + Assert.That(min, Is.EqualTo(0)); + Assert.That(max, Is.EqualTo(expectedMax - 1)); + } + + [Test] + [Category(TestCategories.COVER)] + [Category(TestCategories.NORMAL)] + [Category(TestCategories.INT)] + public void MultithreadedCheckMinMax02() + { + var expectedMin = 2; + var expectedMax = 6; + + var max = int.MinValue; + var min = int.MaxValue; + using var rng = new MultiThreadedRng(); + for(var n = 0; n < 10_000; n++) + { + var value = rng.GetUniformInt(expectedMin, expectedMax); + if (value < min) + min = value; + + if (value > max) + max = value; + + Assert.That(value, Is.GreaterThanOrEqualTo(expectedMin)); + Assert.That(value, Is.LessThanOrEqualTo(expectedMax)); + } + + Assert.That(min, Is.EqualTo(expectedMin)); + Assert.That(max, Is.EqualTo(expectedMax - 1)); + } + + #endregion +} \ No newline at end of file diff --git a/FastRngTests/TestCategories.cs b/FastRngTests/TestCategories.cs index a523612..b416978 100644 --- a/FastRngTests/TestCategories.cs +++ b/FastRngTests/TestCategories.cs @@ -5,6 +5,7 @@ public static class TestCategories public const string COVER = "cover"; public const string PERFORMANCE = "performance"; public const string NORMAL = "normal"; + public const string INT = "int"; public const string EXAMPLE = "example"; public const string LONG_RUNNING = "long running"; } \ No newline at end of file