Refactor FastRandom class with thread-safe shared instance (#986)

- Restructure FastRandom to use interface-based implementation approach
- Add thread-safe Shared static instance for concurrent access
- Enhance documentation with improved explanations about algorithm characteristics
- Extract core logic into LinearCongruentialGeneratorImpl for better maintainability
- Replace Math function calls with MathF equivalents for better performance
- Add parameter validation using new .NET ArgumentOutOfRangeException.ThrowIfNegativeOrZero
- Keep API compatibility with original implementation
This commit is contained in:
Christopher Whitley
2025-05-16 14:54:09 -04:00
committed by GitHub
parent f856694801
commit b88e0d4ab1
+266 -51
View File
@@ -1,127 +1,342 @@
using System;
using System.Runtime.CompilerServices;
using Microsoft.Xna.Framework;
namespace MonoGame.Extended
{
/// <summary>
/// A random number generator that uses a fast algorithm to generate random values.
/// The speed comes at the price of true 'randomness' though, there are noticeable
/// patterns & it compares quite unfavourably to other algorithms in that respect.
/// It's a good choice in situations where speed is more desirable than a
/// good random distribution, and a poor choice when random distribution is important.
/// Represents a pseudo-random number generator using a linear congruential generator algorithm.
/// </summary>
/// <remarks>
/// <para>
/// This implementation uses the same constants as Microsoft Visual C++ rand() function:
///
/// a=214013
/// c=2531011
/// m=2^31
/// </para>
/// <para>
/// It provides high performance and speed, but comes at the price of having lower statistical quality, or true
/// 'randomness' compared to modern algorithms. The algorithm is deterministic based on the initial seed
/// value, making it suitable for reproducible sequences.
///</para>
///<para>
/// Note: This pseudo-random number generator exhibits noticeable patterns and should not be used for
/// cryptographic purposes or when a high-quality random distribution is critical. Consider using
/// <see cref="System.Random"/> for better statistical properties.
/// </para>
/// </remarks>
public class FastRandom
{
private int _state;
private readonly IFastRandomImpl _impl;
public FastRandom()
: this(1)
/// <summary>
/// Provides a thread-safe <see cref="FastRandom"/> instance that may be used concurrently from any thread.
/// </summary>
public static FastRandom Shared { get; } = new FastRandom(new ThreadSafeFastRandomImpl());
/// <summary>
/// Initializes a new instance of the <see cref="FastRandom"/> class using the default seed value.
/// </summary>
public FastRandom() : this(1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FastRandom"/> class using the specified seed value.
/// </summary>
/// <param name="seed">A number used to calculate a starting value for the pseudo-random number sequence.</param>
public FastRandom(int seed)
{
if (seed < 1)
throw new ArgumentOutOfRangeException(nameof(seed), "seed must be greater than zero");
_impl = new LinearCongruentialGeneratorImpl(seed);
}
_state = seed;
private FastRandom(IFastRandomImpl impl)
{
ArgumentNullException.ThrowIfNull(impl);
_impl = impl;
}
/// <summary>
/// Gets the next random integer value.
/// Returns a non-negative random integer.
/// </summary>
/// <returns>A random positive integer.</returns>
/// <returns>A 32-bit signed integer that is greater than or equal to 0 and less than 32768.</returns>
public int Next()
{
_state = 214013*_state + 2531011;
return (_state >> 16) & 0x7FFF;
return _impl.Next();
}
/// <summary>
/// Gets the next random integer value which is greater than zero and less than or equal to
/// the specified maxmimum value.
/// Returns a non-negative random integer that is less than or equal to the specified maximum.
/// </summary>
/// <param name="max">The maximum random integer value to return.</param>
/// <returns>A random integer value between zero and the specified maximum value.</returns>
/// <param name="max">The inclusive upper bound of the random number to be generated.</param>
/// <returns>
/// A 32-bit signed integer that is greater than or equal to 0 and less than or equal to <paramref name="max"/>.
/// </returns>
public int Next(int max)
{
return (int) (max*NextSingle() + 0.5f);
return _impl.Next(max);
}
/// <summary>
/// Gets the next random integer between the specified minimum and maximum values.
/// Returns a random integer that is within a specified range.
/// </summary>
/// <param name="min">The inclusive minimum value.</param>
/// <param name="max">The inclusive maximum value.</param>
/// <param name="min">The inclusive lower bound of the random number returned.</param>
/// <param name="max">The inclusive upper bound of the random number returned.</param>
/// <returns>
/// A 32-bit signed integer that is greater than or equal to <paramref name="min"/> and less than or equal to
/// <paramref name="max"/>.
/// </returns>
public int Next(int min, int max)
{
return (int) ((max - min)*NextSingle() + 0.5f) + min;
return _impl.Next(min, max);
}
/// <summary>
/// Gets the next random integer between the specified range of values.
/// Returns a random integer that is within a specified range.
/// </summary>
/// <param name="range">A range representing the inclusive minimum and maximum values.</param>
/// <returns>A random integer between the specified minumum and maximum values.</returns>
/// <param name="range">
/// A range representing the inclusive lower and upper bound of the random number to return.
/// </param>
/// <returns>
/// A 32-bit signed integer that is greater than or equal to the <see cref="Range{T}.Min"/> and less than or
/// equal to the <see cref="Range{T}.Max"/> value of <paramref name="range"/>.
/// </returns>
public int Next(Range<int> range)
{
return Next(range.Min, range.Max);
return _impl.Next(range);
}
/// <summary>
/// Gets the next random single value.
/// Returns a random floating-point number that is greater than or equal to 0.0 and less than 1.0.
/// </summary>
/// <returns>A random single value between 0 and 1.</returns>
/// <returns>
/// A single-precision floating point number that is greater than or equal to 0.0 and less than 1.0.
/// </returns>
public float NextSingle()
{
return Next()/(float) short.MaxValue;
return _impl.NextSingle();
}
/// <summary>
/// Gets the next random single value which is greater than zero and less than or equal to
/// the specified maxmimum value.
/// Returns a random floating-point number that is greater than or equal to 0.0 and less than the
/// specified maximum.
/// </summary>
/// <param name="max">The maximum random single value to return.</param>
/// <returns>A random single value between zero and the specified maximum value.</returns>
/// <param name="max">The exclusive upper bond of the random number generated.</param>
/// <returns>
/// A single precision floating-point number that is greater than or equal to 0.0 and less than
/// <paramref name="max"/>.
/// </returns>
public float NextSingle(float max)
{
return max*NextSingle();
return _impl.NextSingle(max);
}
/// <summary>
/// Gets the next random single value between the specified minimum and maximum values.
/// Returns a random floating-point number that is within a specified range.
/// </summary>
/// <param name="min">The inclusive minimum value.</param>
/// <param name="max">The inclusive maximum value.</param>
/// <returns>A random single value between the specified minimum and maximum values.</returns>
/// <param name="min">The inclusive lower bound of the random number returned.</param>
/// <param name="max">The exclusive upper bound of the random number returned.</param>
/// <returns>
/// A single-precision floating point number that is greater than or equal to <paramref name="min"/> and
/// less than <paramref name="max"/>.
/// </returns>
public float NextSingle(float min, float max)
{
return (max - min)*NextSingle() + min;
return _impl.NextSingle(min, max);
}
/// <summary>
/// Gets the next random single value between the specified range of values.
/// Returns a random floating-point number that is within a specified range.
/// </summary>
/// <param name="range">A range representing the inclusive minimum and maximum values.</param>
/// <returns>A random single value between the specified minimum and maximum values.</returns>
/// <param name="range">
/// A range representing the inclusive lower and exclusive upper bound of the random number returned.
/// </param>
/// <returns>
/// A single-precision floating point number that is greater than or equal to the <see cref="Range{T}.Min"/>
/// and less than the <see cref="Range{T}.Max"/> value of <paramref name="range"/>
/// </returns>
public float NextSingle(Range<float> range)
{
return NextSingle(range.Min, range.Max);
return _impl.NextSingle(range);
}
/// <summary>
/// Gets the next random angle value.
/// Returns a random angle between -π and π.
/// </summary>
/// <returns>A random angle value.</returns>
/// <returns>
/// A random angle value in radians.
/// </returns>
public float NextAngle()
{
return NextSingle(-MathHelper.Pi, MathHelper.Pi);
return _impl.NextAngle();
}
/// <summary>
/// Gets a random unit vector.
/// </summary>
/// <param name="vector">When this method returns, contains a unit vector with a random direction.</param>
public void NextUnitVector(out Vector2 vector)
{
var angle = NextAngle();
vector = new Vector2((float) Math.Cos(angle), (float) Math.Sin(angle));
_impl.NextUnitVector(out vector);
}
#region IFastRandomImplementation
private interface IFastRandomImpl
{
int Next();
int Next(int max);
int Next(int min, int max);
int Next(Range<int> range);
float NextSingle();
float NextSingle(float max);
float NextSingle(float min, float max);
float NextSingle(Range<float> range);
float NextAngle();
void NextUnitVector(out Vector2 vector);
}
#endregion
#region Linear Congruential Generator
private sealed class LinearCongruentialGeneratorImpl : IFastRandomImpl
{
private const int MULTIPLIER = 214013;
private const int INCREMENT = 2531011;
private int _state;
public LinearCongruentialGeneratorImpl() : this(1)
{
}
public LinearCongruentialGeneratorImpl(int seed)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(seed);
_state = seed;
}
public int Next()
{
_state = MULTIPLIER * _state + INCREMENT;
return (_state >> 16) & 0x7FFF;
}
public int Next(int max)
{
return (int)(max * NextSingle() + 0.5f);
}
public int Next(int min, int max)
{
return (int)((max - min) * NextSingle() + 0.5f) + min;
}
public int Next(Range<int> range)
{
return Next(range.Min, range.Max);
}
public float NextSingle()
{
return Next() / (float)short.MaxValue;
}
public float NextSingle(float max)
{
return max * NextSingle();
}
public float NextSingle(float min, float max)
{
return (max - min) * NextSingle() + min;
}
public float NextSingle(Range<float> range)
{
return NextSingle(range.Min, range.Max);
}
public float NextAngle()
{
return NextSingle(-MathHelper.Pi, MathHelper.Pi);
}
public void NextUnitVector(out Vector2 vector)
{
float angle = NextAngle();
vector.X = MathF.Cos(angle);
vector.Y = MathF.Sin(angle);
}
}
#endregion
#region ThreadSafeImpl
private sealed class ThreadSafeFastRandomImpl : IFastRandomImpl
{
[ThreadStatic]
private static LinearCongruentialGeneratorImpl t_random;
private static LinearCongruentialGeneratorImpl LocalRandom => t_random ?? Create();
[MethodImpl(MethodImplOptions.NoInlining)]
private static LinearCongruentialGeneratorImpl Create()
{
t_random = new LinearCongruentialGeneratorImpl();
return t_random;
}
public int Next()
{
return LocalRandom.Next();
}
public int Next(int max)
{
return LocalRandom.Next(max);
}
public int Next(int min, int max)
{
return LocalRandom.Next(min, max);
}
public int Next(Range<int> range)
{
return LocalRandom.Next(range.Min, range.Max);
}
public float NextSingle()
{
return LocalRandom.NextSingle();
}
public float NextSingle(float max)
{
return LocalRandom.NextSingle(max);
}
public float NextSingle(float min, float max)
{
return LocalRandom.NextSingle(min, max);
}
public float NextSingle(Range<float> range)
{
return LocalRandom.NextSingle(range.Min, range.Max);
}
public float NextAngle()
{
return LocalRandom.NextAngle();
}
public void NextUnitVector(out Vector2 vector)
{
LocalRandom.NextUnitVector(out vector);
}
}
#endregion
}
}
}