From 7a14b607953a5d15eca27954676fb7ce2c89c5aa Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 10 Jul 2023 16:39:56 +0200 Subject: [PATCH] Added a multi-channel, multi-threaded rng for comparison --- FastRng/MultiChannelRng.cs | 168 +++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 FastRng/MultiChannelRng.cs diff --git a/FastRng/MultiChannelRng.cs b/FastRng/MultiChannelRng.cs new file mode 100644 index 0000000..aff1fbc --- /dev/null +++ b/FastRng/MultiChannelRng.cs @@ -0,0 +1,168 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Threading; +using System.Threading.Channels; + +namespace FastRng; + +public sealed class MultiChannelRng : IRandom, IDisposable where TNum : IFloatingPointIeee754, IAdditionOperators +{ +#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 }); + private readonly Channel channelFloats = Channel.CreateBounded(new BoundedChannelOptions(capacity: BUFFER_SIZE) { FullMode = BoundedChannelFullMode.Wait, SingleWriter = true, SingleReader = false }); + + private static readonly TNum CONST_FLOAT_CONVERSION = TNum.CreateChecked(2.328306435454494e-10f); + + // Gets used to stop the producer threads: + private readonly CancellationTokenSource producerTokenSource = new(); + + // The uint producer thread: + private Thread producerRandomUint; + + // The uniform float producer thread: + private Thread producerRandomUniformDistributedFloat; + + // 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 + + /// + /// Creates a multi-threaded random number generator. + /// + /// + /// 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. + /// + public MultiChannelRng() + { + // + // 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.StartProducerThreads(); + } + + /// + /// 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. + ///
+ /// A seed value to generate a deterministic generator. + public MultiChannelRng(uint seedU) + { + this.mW = seedU; + this.mZ = 362_436_069; + this.StartProducerThreads(); + } + + /// + /// 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 MultiChannelRng(uint seedU, uint seedV) + { + this.mW = seedU; + this.mZ = seedV; + this.StartProducerThreads(); + } + + private void StartProducerThreads() + { + this.producerRandomUint = new Thread(() => this.RandomProducerUint(this.producerTokenSource.Token)) {IsBackground = true}; + this.producerRandomUint.Start(); + this.producerRandomUniformDistributedFloat = new Thread(() => this.RandomProducerUniformDistributedFloat(this.producerTokenSource.Token)) {IsBackground = true}; + this.producerRandomUniformDistributedFloat.Start(); + } + + #endregion + + #region Producers + + [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) + { + } + } + + [ExcludeFromCodeCoverage] + private async void RandomProducerUniformDistributedFloat(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + await this.channelFloats.Writer.WriteAsync((TNum.CreateChecked(await this.channelIntegers.Reader.ReadAsync(cancellationToken)) + TNum.One) * CONST_FLOAT_CONVERSION, cancellationToken); + } + } + catch (OperationCanceledException) + { + } + } + + #endregion + + #region Implementing interfaces + + #region Implementation of IDisposable + + private void StopProducer() => this.producerTokenSource.Cancel(); + + public void Dispose() => this.StopProducer(); + + #endregion + + #region Implementation of IRandom + + public TNum GetUniform(CancellationToken cancel = default) + { + var valueTask = this.channelFloats.Reader.ReadAsync(cancel); + return valueTask.AsTask().Result; + } + + #endregion + + #endregion +} \ No newline at end of file