Compare commits

..

13 Commits
v1.1.1 ... main

Author SHA1 Message Date
639425c67d Merge branch '9-add-possibility-to-get-integers' into 'main'
Resolve "Add possibility to get integers"

Closes #9 and #10

See merge request open-source/dotnet/FastRng!7
2024-05-29 09:40:28 +00:00
Thorsten Sommer
262dd6cf1f
Prepared next release 2024-05-29 11:39:18 +02:00
Thorsten Sommer
b2d8e2b5ed
Added possibility to get integers out of the RNG 2024-05-29 11:39:08 +02:00
Thorsten Sommer
f9ea837f68
Added documentation 2024-05-29 11:37:59 +02:00
Thorsten Sommer
8ade2f5a5d
Addressed some warnings 2024-05-29 11:37:29 +02:00
Thorsten Sommer
817895015f
Spelling 2024-05-29 11:36:57 +02:00
Thorsten Sommer
a8121a306e
.NET 8 migration; closes #10 2024-05-29 11:34:37 +02:00
Thorsten Sommer
4602bb14af
Added a frequency analysis tool for integers 2024-05-29 11:33:12 +02:00
Thorsten Sommer
0d1d48db3b
Spelling 2024-05-29 11:32:25 +02:00
d8f024a5c5 Merge branch '8-bug-indexoutofrangeexception-when-using-multi-threaded-consumers' into 'main'
Resolve "Bug: IndexOutOfRangeException when using multi-threaded consumers"

Closes #8

See merge request open-source/dotnet/FastRng!6
2023-11-14 14:43:58 +00:00
Thorsten Sommer
f1fe1de090
Increment version 2023-11-14 15:43:11 +01:00
Thorsten Sommer
a838dacf2a
Fixed rare index bug when using multiple multithreading consumers 2023-11-14 15:38:41 +01:00
Thorsten Sommer
faf6c99c19
Added unit test to cover issue #8 2023-11-14 13:57:37 +01:00
11 changed files with 662 additions and 39 deletions

View File

@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>1.1.1</AssemblyVersion>
<FileVersion>1.1.1</FileVersion>
<PackageVersion>1.1.1</PackageVersion>
<AssemblyVersion>1.2.0</AssemblyVersion>
<FileVersion>1.2.0</FileVersion>
<PackageVersion>1.2.0</PackageVersion>
<Authors>Thorsten Sommer</Authors>
<PackageProjectUrl>https://devops.tsommer.org/open-source/dotnet/FastRng</PackageProjectUrl>
<RepositoryUrl>https://devops.tsommer.org/open-source/dotnet/FastRng</RepositoryUrl>

View File

@ -18,4 +18,29 @@ public interface IRandom<TNum> : IDisposable where TNum : IFloatingPointIeee754<
/// by using multiple threads at the same time.
/// </remarks>
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

@ -4,6 +4,8 @@ using System.Numerics;
using System.Threading;
using System.Threading.Channels;
// ReSharper disable RedundantExtendsListEntry
namespace FastRng;
public sealed class MultiChannelRng<TNum> : IRandom<TNum>, IDisposable where TNum : IFloatingPointIeee754<TNum>, IAdditionOperators<TNum, TNum, TNum>
@ -36,13 +38,12 @@ public sealed class MultiChannelRng<TNum> : IRandom<TNum>, IDisposable where TNu
#region Constructors
/// <summary>
/// Creates a multi-threaded random number generator.
/// Creates a multithreaded random number generator.
/// </summary>
/// <remarks>
/// This constructor uses the user's current local time
/// to derive necessary parameters for the generator.
/// Thus, the results are depending on the time, where
/// the generator was created.
/// to derive the necessary parameters for the generator.
/// Thus, the results depend on the time when the generator was created.
/// </remarks>
public MultiChannelRng()
{
@ -58,7 +59,7 @@ public sealed class MultiChannelRng<TNum> : IRandom<TNum>, IDisposable where TNu
}
/// <summary>
/// Creates a multi-threaded random number generator.
/// Creates a multithreaded random number generator.
/// </summary>
/// <remarks>
/// A multi-threaded random number generator created by this constructor is
@ -149,19 +150,35 @@ public sealed class MultiChannelRng<TNum> : IRandom<TNum>, IDisposable where TNu
#region Implementation of IDisposable
private void StopProducer() => this.producerTokenSource.Cancel();
/// <inheritdoc />
public void Dispose() => this.StopProducer();
#endregion
#region Implementation of IRandom<TNum>
/// <inheritdoc />
public TNum GetUniform(CancellationToken cancel = default)
{
var valueTask = this.channelFloats.Reader.ReadAsync(cancel);
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

View File

@ -5,6 +5,8 @@ using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
// ReSharper disable RedundantExtendsListEntry
namespace FastRng;
/// <summary>
@ -12,18 +14,15 @@ namespace FastRng;
/// </summary>
/// <remarks>
/// Please note, that Math.NET's (https://www.mathdotnet.com/) random number generator is in some situations faster.
/// Unlike Math.NET, MultiThreadedRng is multi-threaded. Consumers can use a token to cancel e.g. timeout an operation.<br/><br/>
/// Unlike Math.NET, MultiThreadedRng is multithreaded. Consumers can use a token to cancel an operation.<br/><br/>
///
/// MultiThreadedRng using a shape fitter (a rejection sampler) to enforce arbitrary shapes of probabilities for
/// desired distributions. By using the shape fitter, it is even easy to define discontinuous, arbitrary functions
/// desired distributions. By using the shape fitter, it is even easier to define discontinuous, arbitrary functions
/// as shapes. Any consumer can define and use own distributions.<br/><br/>
///
/// This class uses the George Marsaglia's MWC algorithm. The algorithm's implementation based loosely on John D.
/// This class uses the George Marsaglia's MWC algorithm. The algorithm's implementation is based loosely on John D.
/// Cook's (johndcook.com) implementation (https://www.codeproject.com/Articles/25172/Simple-Random-Number-Generation).
/// Thanks John for the inspiration.<br/><br/>
///
/// Please notice: When using the debug environment, MultiThreadedRng might uses a smaller buffer size. Please ensure,
/// that the production environment uses a release build, though.
/// Thanks, John, for the inspiration.<br/><br/>
/// </remarks>
public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TNum : IFloatingPointIeee754<TNum>, IAdditionOperators<TNum, TNum, TNum>
{
@ -33,14 +32,17 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
private const int BUFFER_SIZE = 1_000_000;
#endif
// The queue size means, how many buffer we store in a queue at the same time:
// The queue size means, how many buffers we store in a queue at the same time:
private const int QUEUE_SIZE_FLOAT = 2;
// The queue size means, how many buffer we store in a queue at the same time:
// The queue size means, how many buffers we store in a queue at the same time:
private const int QUEUE_SIZE_INT = QUEUE_SIZE_FLOAT * 2;
private static readonly TNum CONST_FLOAT_CONVERSION = TNum.CreateChecked(2.328306435454494e-10f);
// ReSharper disable StaticMemberInGenericType
private static readonly object LOCKER = new();
// ReSharper restore StaticMemberInGenericType
// Gets used to stop the producer threads:
private readonly CancellationTokenSource producerTokenSource = new();
@ -59,14 +61,16 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, 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)
private uint mW;
private uint mZ;
// This is the current buffer for the consumer side i.e. the public interfaces:
private TNum[] currentBuffer = Array.Empty<TNum>();
// This is the current buffer for the consumer side, i.e., the public interfaces:
private TNum[] currentBuffer = [];
// The current pointer to the next current buffer's address to read from:
private int currentBufferPointer = BUFFER_SIZE;
@ -77,10 +81,8 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
/// Creates a multi-threaded random number generator.
/// </summary>
/// <remarks>
/// This constructor uses the user's current local time
/// to derive necessary parameters for the generator.
/// Thus, the results are depending on the time, where
/// the generator was created.
/// This constructor uses the user's current local time to derive the necessary parameters for the generator.
/// Thus, the results depend on the time when the generator was created.
/// </remarks>
public MultiThreadedRng()
{
@ -171,7 +173,7 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
{
try
{
// Ensure, that we do not produce more buffers, as configured:
// Ensure that we do not produce more buffers, as configured:
if (this.queueIntegers.Count < QUEUE_SIZE_INT)
{
this.queueIntegers.Enqueue(nextBuffer);
@ -202,7 +204,7 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
while (!cancellationToken.IsCancellationRequested)
{
// A local source buffer of uints:
uint[] bufferSource = null;
uint[] bufferSource;
// Try to get the next source buffer:
while (!this.queueIntegers.TryDequeue(out bufferSource) && !cancellationToken.IsCancellationRequested)
@ -224,7 +226,7 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
{
try
{
// Ensure, that the queue contains only the configured number of buffers:
// Ensure that the queue contains only the configured number of buffers:
if (this.queueFloats.Count < QUEUE_SIZE_FLOAT)
{
this.queueFloats.Enqueue(nextBuffer);
@ -273,7 +275,7 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
// can get the next buffer:
lock (LOCKER)
{
// We are might not the first thread, which has to get the next buffer.
// We might not the first thread, which has to get the next buffer.
// When some other thread has already got the next buffer, the pointer
// was already reset to zero. In this case, we start over again:
if(this.currentBufferPointer < BUFFER_SIZE)
@ -295,6 +297,11 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
// Made a local copy of the current pointer:
var myPointer = this.currentBufferPointer;
// Issue #8: This might happen when another thread interrupted the current thread, and
// the other thread has already updated the pointer. In this case, we start over again.
if (myPointer >= BUFFER_SIZE)
goto Start;
// Increment the pointer for the next thread or call:
var nextPointer = myPointer + 1;
@ -303,10 +310,24 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
goto Start;
//
// Case: Success. We updated the pointer and, thus, can use the read number.
// Case: Success. We updated the pointer and, thus, can use it to read the number.
//
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();
@ -315,7 +336,11 @@ public sealed class MultiThreadedRng<TNum> : IRandom<TNum>, IDisposable where TN
/// when it is no longer needed. Otherwise, the background threads
/// are still running.
/// </summary>
public void Dispose() => this.StopProducer();
public void Dispose()
{
this.independentUIntProducer.Dispose();
this.StopProducer();
}
#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
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>

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

@ -0,0 +1,82 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
namespace FastRngTests;
[ExcludeFromCodeCoverage]
public sealed class IntFrequencyAnalysis
{
private readonly uint[] data;
public IntFrequencyAnalysis(int maxValue = 100)
{
this.data = new uint[maxValue];
}
public void CountThis(int value)
{
this.data[value]++;
}
public float[] GetNormalizedEvents()
{
var max = (float)this.data.Max();
var result = new float[this.data.Length];
for (var n = 0; n < this.data.Length; n++)
{
result[n] = this.data[n] / max;
}
return result;
}
private float[] Normalize()
{
var max = (float)this.data.Max();
var result = new float[this.data.Length];
for (var n = 0; n < this.data.Length; n++)
result[n] = this.data[n] / max;
return result;
}
public float[] NormalizeAndPlotEvents(Action<string> writer)
{
var result = this.Normalize();
Plot(result, writer, "Event Distribution");
return result;
}
public void PlotOccurence(Action<string> writer)
{
var data = this.data.Select(n => n > 0f ? 1.0f : 0.0f).ToArray();
Plot(data, writer, "Occurrence Distribution");
}
private static void Plot(float[] data, Action<string> writer, string name)
{
const int HEIGHT = 16;
var values = new float[data.Length];
for (var n = 0; n < data.Length; n++)
{
values[n] = data[n] * HEIGHT;
}
var sb = new StringBuilder();
for (var line = HEIGHT; line > 0; line--)
{
for (var column = 0; column < data.Length; column++)
sb.Append(values[column] >= line ? '█' : '░');
writer.Invoke(sb.ToString());
sb.Clear();
}
writer.Invoke(name);
writer.Invoke(string.Empty);
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using FastRng;
using FastRng.Distributions;
using NUnit.Framework;
@ -222,6 +223,32 @@ public class MultiThreadedRngTests
Assert.That(lorentzContains0, Is.False, "Lorentz distribution contained 0");
Assert.That(lorentzContains1, Is.True, "Lorentz distribution does not contained 1");
}
[Test]
[Category(TestCategories.LONG_RUNNING)]
public void TestMultiThreadedConsumer()
{
var job1 = Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning);
var job2 = Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning);
var job3 = Task.Factory.StartNew(Run, TaskCreationOptions.LongRunning);
Assert.DoesNotThrowAsync(async () => await job1);
Assert.DoesNotThrowAsync(async () => await job2);
Assert.DoesNotThrowAsync(async () => await job3);
return;
float Run()
{
var sum = 0f;
var distLorentz = new CauchyLorentzX1<float>(this.rng);
for (int i = 0; i < 100_000_000; i++)
{
sum += distLorentz.NextNumber();
}
return sum;
}
}
[Test]
[Category(TestCategories.COVER)]

View File

@ -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";
}

View File

@ -1,14 +1,12 @@
# FastRng
FastRng is a multi-threaded pseudo-random number generator. Besides the generation of uniformly distributed random numbers, there are several other distributions to choose from. For performance reasons the parameters of the distributions are not user-definable. For some distributions, therefore, different parameter variations are available. If a different combination is desired, a separate class can be created.
FastRng is a multithreaded pseudo-random number generator. Besides the generation of uniformly distributed random numbers, there are several other distributions to choose from. For performance reasons, the parameters of the distributions are not user-definable. For some distributions, therefore, different parameter variations are available. If a different combination is desired, a separate class can be created.
Please note, that Math.NET's (https://www.mathdotnet.com/) random number generator is in some situations faster. Unlike Math.NET, MultiThreadedRng is multi-threaded. Consumers can use a token to cancel e.g. timeout an operation.
Please note, that Math.NET's (https://www.mathdotnet.com/) random number generator is in some situations faster. Unlike Math.NET, MultiThreadedRng is multithreaded. Consumers can use a token to cancel an operation.
FastRng (class `MultiThreadedRng`) using a shape fitter (a rejection sampler) to enforce arbitrary shapes of probabilities for desired distributions. By using the shape fitter, it is even easy to define discontinuous, arbitrary functions as shapes. Any consumer can define and use own distributions.
FastRng (class `MultiThreadedRng`) using a shape fitter (a rejection sampler) to enforce arbitrary shapes of probabilities for desired distributions. By using the shape fitter, it is even easier to define discontinuous, arbitrary functions as shapes. Any consumer can define and use own distributions.
The class `MultiThreadedRng` 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 the inspiration.
Please notice: When using the debug environment, MultiThreadedRng might uses a smaller buffer size. Please ensure, that the production environment uses a release build, though.
The class `MultiThreadedRng` 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 the inspiration.
## Usage
Example code:
@ -37,7 +35,7 @@ Notes:
- `MultiThreadedRng` fills some buffers after creation. Thus, create and reuse it as long as needed. Avoid useless re-creation.
- Distributions need some time at creation to calculate probabilities. Thus, just create a distribution once and use reuse it. Avoid useless re-creation.
- Distributions need some time in creation to calculate probabilities. Thus, create a distribution once and use reuse it. Avoid useless re-creation.
## Available Distributions