Skip to content

Commit 6310c20

Browse files
committed
Improve Random (performance, APIs, ...)
- Changes the algorithm used by `new Random()` to be one that's smaller and faster and produces better results - Refactors the implementation to make it internally pluggable - Moves the existing implementation to be used when it's necessary for back compat - Adds NextInt64 and NextSingle methods
1 parent 1a39322 commit 6310c20

File tree

8 files changed

+1304
-230
lines changed

8 files changed

+1304
-230
lines changed

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
<Compile Include="$(MSBuildThisFileDirectory)System\PlatformNotSupportedException.cs" />
484484
<Compile Include="$(MSBuildThisFileDirectory)System\Progress.cs" />
485485
<Compile Include="$(MSBuildThisFileDirectory)System\Random.cs" />
486+
<Compile Include="$(MSBuildThisFileDirectory)System\Random.ImplBase.cs" />
487+
<Compile Include="$(MSBuildThisFileDirectory)System\Random.LegacyImpl.cs" />
488+
<Compile Include="$(MSBuildThisFileDirectory)System\Random.Xoshiro128StarStarImpl.cs" />
489+
<Compile Include="$(MSBuildThisFileDirectory)System\Random.Xoshiro256StarStarImpl.cs" />
486490
<Compile Include="$(MSBuildThisFileDirectory)System\Range.cs" />
487491
<Compile Include="$(MSBuildThisFileDirectory)System\RankException.cs" />
488492
<Compile Include="$(MSBuildThisFileDirectory)System\ReadOnlyMemory.cs" />
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Numerics;
5+
using System.Runtime.CompilerServices;
6+
7+
namespace System
8+
{
9+
public partial class Random
10+
{
11+
/// <summary>Base type for all generator implementations that plug into the base Random.</summary>
12+
internal abstract class ImplBase
13+
{
14+
public abstract double Sample();
15+
16+
public abstract int Next();
17+
18+
public abstract int Next(int maxValue);
19+
20+
public abstract int Next(int minValue, int maxValue);
21+
22+
public abstract long NextInt64();
23+
24+
public abstract long NextInt64(long maxValue);
25+
26+
public abstract long NextInt64(long minValue, long maxValue);
27+
28+
public abstract float NextSingle();
29+
30+
public abstract double NextDouble();
31+
32+
public abstract void NextBytes(byte[] buffer);
33+
34+
public abstract void NextBytes(Span<byte> buffer);
35+
36+
[MethodImpl(MethodImplOptions.AggressiveInlining)] // hot path, only a handful of callers, not otherwise inlined, and intrinsics result in a small amount of asm
37+
protected static int Log2Ceiling(uint value)
38+
{
39+
int result = BitOperations.Log2(value);
40+
if (BitOperations.PopCount(value) != 1)
41+
{
42+
result++;
43+
}
44+
return result;
45+
}
46+
47+
[MethodImpl(MethodImplOptions.AggressiveInlining)] // hot path, only a handful of callers, not otherwise inlined, and intrinsics result in a small amount of asm
48+
protected static int Log2Ceiling(ulong value)
49+
{
50+
int result = BitOperations.Log2(value);
51+
if (BitOperations.PopCount(value) != 1)
52+
{
53+
result++;
54+
}
55+
return result;
56+
}
57+
}
58+
}
59+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace System
7+
{
8+
public partial class Random
9+
{
10+
/// <summary>
11+
/// Provides an implementation used for compatibility with cases where either a) the
12+
/// sequence of numbers could be predicted based on the algorithm employed historically and
13+
/// thus expected (e.g. a specific seed used in tests) or b) where a derived type may
14+
/// reasonably expect its overrides to be called. The algorithm is based on a modified version
15+
/// of Knuth's subtractive random number generator algorithm. See https://github.com/dotnet/runtime/issues/23198
16+
/// for a discussion of some of the modifications / discrepancies.
17+
/// </summary>
18+
private sealed class LegacyImpl : ImplBase
19+
{
20+
[ThreadStatic]
21+
private static Xoshiro128StarStarImpl? t_seedGenerator;
22+
23+
/// <summary>Reference to the <see cref="Random"/> containing this implementation instance.</summary>
24+
/// <remarks>Used to ensure that any calls to other virtual members are performed using the Random-derived instance, if one exists.</remarks>
25+
private readonly Random _parent;
26+
private readonly int[] _seedArray;
27+
private int _inext;
28+
private int _inextp;
29+
30+
public LegacyImpl(Random parent) : this(parent, (t_seedGenerator ??= new()).Next())
31+
{
32+
}
33+
34+
public LegacyImpl(Random parent, int Seed)
35+
{
36+
_parent = parent;
37+
38+
// Initialize seed array.
39+
int[] seedArray = _seedArray = new int[56];
40+
41+
int subtraction = (Seed == int.MinValue) ? int.MaxValue : Math.Abs(Seed);
42+
int mj = 161803398 - subtraction; // magic number based on Phi (golden ratio)
43+
seedArray[55] = mj;
44+
int mk = 1;
45+
46+
int ii = 0;
47+
for (int i = 1; i < 55; i++)
48+
{
49+
// The range [1..55] is special (Knuth) and so we're wasting the 0'th position.
50+
if ((ii += 21) >= 55)
51+
{
52+
ii -= 55;
53+
}
54+
55+
seedArray[ii] = mk;
56+
mk = mj - mk;
57+
if (mk < 0)
58+
{
59+
mk += int.MaxValue;
60+
}
61+
62+
mj = seedArray[ii];
63+
}
64+
65+
for (int k = 1; k < 5; k++)
66+
{
67+
for (int i = 1; i < 56; i++)
68+
{
69+
int n = i + 30;
70+
if (n >= 55)
71+
{
72+
n -= 55;
73+
}
74+
75+
seedArray[i] -= seedArray[1 + n];
76+
if (seedArray[i] < 0)
77+
{
78+
seedArray[i] += int.MaxValue;
79+
}
80+
}
81+
}
82+
83+
_inextp = 21;
84+
}
85+
86+
public override double Sample() =>
87+
// Including the division at the end gives us significantly improved random number distribution.
88+
InternalSample() * (1.0 / int.MaxValue);
89+
90+
public override int Next() => InternalSample();
91+
92+
public override int Next(int maxValue) => (int)(_parent.Sample() * maxValue);
93+
94+
public override int Next(int minValue, int maxValue)
95+
{
96+
long range = (long)maxValue - minValue;
97+
return range <= int.MaxValue ?
98+
(int)(_parent.Sample() * range) + minValue :
99+
(int)((long)(GetSampleForLargeRange() * range) + minValue);
100+
}
101+
102+
public override long NextInt64()
103+
{
104+
while (true)
105+
{
106+
// Get top 63 bits to get a value in the range [0, long.MaxValue], but try again
107+
// if the value is actually long.MaxValue, as the method is defined to return a value
108+
// in the range [0, long.MaxValue).
109+
ulong result = NextUInt64() >> 1;
110+
if (result != long.MaxValue)
111+
{
112+
return (long)result;
113+
}
114+
}
115+
}
116+
117+
public override long NextInt64(long maxValue) => NextInt64(0, maxValue);
118+
119+
public override long NextInt64(long minValue, long maxValue)
120+
{
121+
ulong exclusiveRange = (ulong)(maxValue - minValue);
122+
123+
if (exclusiveRange > 1)
124+
{
125+
// Narrow down to the smallest range [0, 2^bits] that contains maxValue - minValue
126+
// Then repeatedly generate a value in that outer range until we get one within the inner range.
127+
int bits = Log2Ceiling(exclusiveRange);
128+
while (true)
129+
{
130+
ulong result = NextUInt64() >> (sizeof(long) * 8 - bits);
131+
if (result < exclusiveRange)
132+
{
133+
return (long)result + minValue;
134+
}
135+
}
136+
}
137+
138+
Debug.Assert(minValue == maxValue || minValue + 1 == maxValue);
139+
return minValue;
140+
}
141+
142+
/// <summary>Produces a value in the range [0, ulong.MaxValue].</summary>
143+
private unsafe ulong NextUInt64()
144+
{
145+
Span<byte> resultBytes = stackalloc byte[8];
146+
NextBytes(resultBytes);
147+
return BitConverter.ToUInt64(resultBytes);
148+
}
149+
150+
public override double NextDouble() => _parent.Sample();
151+
152+
public override float NextSingle() => (float)_parent.Sample();
153+
154+
public override void NextBytes(byte[] buffer)
155+
{
156+
for (int i = 0; i < buffer.Length; i++)
157+
{
158+
buffer[i] = (byte)InternalSample();
159+
}
160+
}
161+
162+
public override void NextBytes(Span<byte> buffer)
163+
{
164+
for (int i = 0; i < buffer.Length; i++)
165+
{
166+
buffer[i] = (byte)_parent.Next();
167+
}
168+
}
169+
170+
private int InternalSample()
171+
{
172+
int locINext = _inext;
173+
if (++locINext >= 56)
174+
{
175+
locINext = 1;
176+
}
177+
178+
int locINextp = _inextp;
179+
if (++locINextp >= 56)
180+
{
181+
locINextp = 1;
182+
}
183+
184+
int[] seedArray = _seedArray;
185+
int retVal = seedArray[locINext] - seedArray[locINextp];
186+
187+
if (retVal == int.MaxValue)
188+
{
189+
retVal--;
190+
}
191+
if (retVal < 0)
192+
{
193+
retVal += int.MaxValue;
194+
}
195+
196+
seedArray[locINext] = retVal;
197+
_inext = locINext;
198+
_inextp = locINextp;
199+
200+
return retVal;
201+
}
202+
203+
private double GetSampleForLargeRange()
204+
{
205+
// The distribution of the double returned by Sample is not good enough for a large range.
206+
// If we use Sample for a range [int.MinValue..int.MaxValue), we will end up getting even numbers only.
207+
int result = InternalSample();
208+
209+
// We can't use addition here: the distribution will be bad if we do that.
210+
if (InternalSample() % 2 == 0) // decide the sign based on second sample
211+
{
212+
result = -result;
213+
}
214+
215+
double d = result;
216+
d += int.MaxValue - 1; // get a number in range [0..2*int.MaxValue-1)
217+
d /= 2u * int.MaxValue - 1;
218+
return d;
219+
}
220+
}
221+
}
222+
}

0 commit comments

Comments
 (0)