Skip to content

Commit

Permalink
Lazily initialize base state for Random-derived type (#81627)
Browse files Browse the repository at this point in the history
When the legacy Random algorithm is used, which happens when a seed is supplied or when a type derived from Random is used, the state for the algorithm is initialized, including an int[56] that gets allocated.  For a Random-derived type that overrides all of the base methods, however, that state is never used.  We can create it lazily rather than at ctor time and avoid those base costs if they're never used.

(I'd previously made several attempts at this, but each ended up with regressions up to 15%.  Any regressions here appear to be within the noise, and even if they manifest, would only be limited to the case of Random-derived types that don't supply their own implementations.)
  • Loading branch information
stephentoub committed Feb 14, 2023
1 parent faae476 commit fc1ce86
Showing 1 changed file with 58 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;

Expand All @@ -18,7 +19,7 @@ private sealed class Net5CompatSeedImpl : ImplBase
private CompatPrng _prng; // mutable struct; do not make this readonly

public Net5CompatSeedImpl(int seed) =>
_prng = new CompatPrng(seed);
_prng.EnsureInitialized(seed);

public override double Sample() => _prng.Sample();

Expand Down Expand Up @@ -98,25 +99,40 @@ private sealed class Net5CompatDerivedImpl : ImplBase
/// <summary>Reference to the <see cref="Random"/> containing this implementation instance.</summary>
/// <remarks>Used to ensure that any calls to other virtual members are performed using the Random-derived instance, if one exists.</remarks>
private readonly Random _parent;
/// <summary>Potentially lazily-initialized algorithm backing this instance.</summary>
/// <summary>Seed specified at construction time used to lazily initialize <see cref="_prng"/>.</summary>
private readonly int _seed;
/// <summary>Lazily-initialized algorithm backing this instance.</summary>
private CompatPrng _prng; // mutable struct; do not make this readonly

public Net5CompatDerivedImpl(Random parent) : this(parent, Shared.Next()) { }

public Net5CompatDerivedImpl(Random parent, int seed)
{
_parent = parent;
_prng = new CompatPrng(seed);
_seed = seed;
}

public override double Sample() => _prng.Sample();
public override double Sample()
{
_prng.EnsureInitialized(_seed);
return _prng.Sample();
}

public override int Next() => _prng.InternalSample();
public override int Next()
{
_prng.EnsureInitialized(_seed);
return _prng.InternalSample();
}

public override int Next(int maxValue) => (int)(_parent.Sample() * maxValue);
public override int Next(int maxValue)
{
_prng.EnsureInitialized(_seed);
return (int)(_parent.Sample() * maxValue);
}

public override int Next(int minValue, int maxValue)
{
_prng.EnsureInitialized(_seed);
long range = (long)maxValue - minValue;
return range <= int.MaxValue ?
(int)(_parent.Sample() * range) + minValue :
Expand All @@ -125,6 +141,7 @@ public override int Next(int minValue, int maxValue)

public override long NextInt64()
{
_prng.EnsureInitialized(_seed);
while (true)
{
// Get top 63 bits to get a value in the range [0, long.MaxValue], but try again
Expand All @@ -146,6 +163,8 @@ public override long NextInt64(long minValue, long maxValue)

if (exclusiveRange > 1)
{
_prng.EnsureInitialized(_seed);

// Narrow down to the smallest range [0, 2^bits] that contains maxValue - minValue
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling(exclusiveRange);
Expand All @@ -169,14 +188,27 @@ public override long NextInt64(long minValue, long maxValue)
(((ulong)(uint)_parent.Next(1 << 22)) << 22) |
(((ulong)(uint)_parent.Next(1 << 20)) << 44);

public override double NextDouble() => _parent.Sample();
public override double NextDouble()
{
_prng.EnsureInitialized(_seed);
return _parent.Sample();
}

public override float NextSingle() => (float)_parent.Sample();
public override float NextSingle()
{
_prng.EnsureInitialized(_seed);
return (float)_parent.Sample();
}

public override void NextBytes(byte[] buffer) => _prng.NextBytes(buffer);
public override void NextBytes(byte[] buffer)
{
_prng.EnsureInitialized(_seed);
_prng.NextBytes(buffer);
}

public override void NextBytes(Span<byte> buffer)
{
_prng.EnsureInitialized(_seed);
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)_parent.Next();
Expand All @@ -191,13 +223,25 @@ public override void NextBytes(Span<byte> buffer)
/// </summary>
private struct CompatPrng
{
private int[] _seedArray;
private int[]? _seedArray;
private int _inext;
private int _inextp;

public CompatPrng(int seed)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MemberNotNull(nameof(_seedArray))]
internal void EnsureInitialized(int seed)
{
// Initialize seed array.
if (_seedArray is null)
{
Initialize(seed);
}
}

[MemberNotNull(nameof(_seedArray))]
private void Initialize(int seed)
{
Debug.Assert(_seedArray is null);

int[] seedArray = new int[56];

int subtraction = (seed == int.MinValue) ? int.MaxValue : Math.Abs(seed);
Expand Down Expand Up @@ -261,6 +305,8 @@ internal void NextBytes(Span<byte> buffer)

internal int InternalSample()
{
Debug.Assert(_seedArray is not null);

int locINext = _inext;
if (++locINext >= 56)
{
Expand Down

0 comments on commit fc1ce86

Please sign in to comment.