Added possibility to get integers out of the RNG

This commit is contained in:
Thorsten Sommer 2024-05-29 11:39:08 +02:00
parent f9ea837f68
commit b2d8e2b5ed
No known key found for this signature in database
GPG Key ID: B0B7E2FC074BF1F5
6 changed files with 509 additions and 1 deletions

View File

@ -18,4 +18,29 @@ public interface IRandom<TNum> : IDisposable where TNum : IFloatingPointIeee754<
/// by using multiple threads at the same time. /// by using multiple threads at the same time.
/// </remarks> /// </remarks>
public TNum GetUniform(CancellationToken cancel = default); public TNum GetUniform(CancellationToken cancel = default);
/// <summary>
/// Get a uniform distributed pseudo-random number from the interval [0, max).
/// </summary>
/// <remarks>
/// This method is thread-safe. You can consume numbers from the same generator
/// by using multiple threads at the same time.
/// </remarks>
/// <param name="max">The maximum value (exclusive). The max value returned will be max - 1.</param>
/// <param name="cancel">The cancellation token.</param>
/// <returns>A pseudo-random number from the interval [0, max).</returns>
public int GetUniformInt(int max, CancellationToken cancel = default);
/// <summary>
/// Get a uniform distributed pseudo-random number from the interval [min, max).
/// </summary>
/// <remarks>
/// This method is thread-safe. You can consume numbers from the same generator
/// by using multiple threads at the same time.
/// </remarks>
/// <param name="min">The minimum value (inclusive).</param>
/// <param name="max">The maximum value (exclusive). The max value returned will be max - 1.</param>
/// <param name="cancel">The cancellation token.</param>
/// <returns>A pseudo-random number from the interval [min, max).</returns>
public int GetUniformInt(int min, int max, CancellationToken cancel = default);
} }

View File

@ -165,6 +165,20 @@ public sealed class MultiChannelRng<TNum> : IRandom<TNum>, IDisposable where TNu
return valueTask.AsTask().Result; return valueTask.AsTask().Result;
} }
/// <inheritdoc />
public int GetUniformInt(int max, CancellationToken cancel = default)
{
var valueTask = this.channelIntegers.Reader.ReadAsync(cancel);
return (int) (valueTask.AsTask().Result % (uint) max);
}
/// <inheritdoc />
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
#endregion #endregion

View File

@ -62,6 +62,8 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
// The uniform float producer thread: // The uniform float producer thread:
private Thread producerRandomUniformDistributedFloat; private Thread producerRandomUniformDistributedFloat;
private readonly UIntChannelProducer independentUIntProducer = new();
// Variable w and z for the uint generator. Both get used // Variable w and z for the uint generator. Both get used
// as seeding variable as well (cf. constructors) // as seeding variable as well (cf. constructors)
private uint mW; private uint mW;
@ -313,6 +315,20 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
return this.currentBuffer[myPointer]; return this.currentBuffer[myPointer];
} }
/// <inheritdoc />
public int GetUniformInt(int max, CancellationToken cancel = default)
{
var valueTask = this.independentUIntProducer.GetNextAsync(cancel);
return (int) (valueTask.AsTask().Result % (uint) max);
}
/// <inheritdoc />
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(); private void StopProducer() => this.producerTokenSource.Cancel();
/// <summary> /// <summary>
@ -320,7 +336,11 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
/// when it is no longer needed. Otherwise, the background threads /// when it is no longer needed. Otherwise, the background threads
/// are still running. /// are still running.
/// </summary> /// </summary>
public void Dispose() => this.StopProducer(); public void Dispose()
{
this.independentUIntProducer.Dispose();
this.StopProducer();
}
#endregion #endregion
} }

View File

@ -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<uint> channelIntegers = Channel.CreateBounded<uint>(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();
}
/// <summary>
/// Creates a multithreaded random number generator.
/// </summary>
/// <remarks>
/// A multi-threaded random number generator created by this constructor is
/// deterministic. It's behaviour is not depending on the time of its creation.<br/><br/>
///
/// <b>Please note:</b> 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.
/// </remarks>
/// <param name="seedU">A seed value to generate a deterministic generator.</param>
public UIntChannelProducer(uint seedU)
{
this.mW = seedU;
this.mZ = 362_436_069;
this.StartProducerThread();
}
/// <summary>
/// Creates a multi-threaded random number generator.
/// </summary>
/// <remarks>
/// A multi-threaded random number generator created by this constructor is
/// deterministic. It's behaviour is not depending on the time of its creation.<br/><br/>
///
/// <b>Please note:</b> 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.
/// </remarks>
/// <param name="seedU">The first seed value.</param>
/// <param name="seedV">The second seed value.</param>
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<uint> GetNextAsync(CancellationToken cancellationToken = default) => await this.channelIntegers.Reader.ReadAsync(cancellationToken);
#region Implementation of IDisposable
private void StopProducer() => this.producerTokenSource.Cancel();
/// <inheritdoc />
public void Dispose() => this.StopProducer();
#endregion
}

327
FastRngTests/GetIntTests.cs Normal file
View File

@ -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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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<float>();
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
}

View File

@ -5,6 +5,7 @@ public static class TestCategories
public const string COVER = "cover"; public const string COVER = "cover";
public const string PERFORMANCE = "performance"; public const string PERFORMANCE = "performance";
public const string NORMAL = "normal"; public const string NORMAL = "normal";
public const string INT = "int";
public const string EXAMPLE = "example"; public const string EXAMPLE = "example";
public const string LONG_RUNNING = "long running"; public const string LONG_RUNNING = "long running";
} }