Skip to content

Commit

Permalink
Implementation of Lemire's nearly divisionless method (#79790)
Browse files Browse the repository at this point in the history
* Lemire implementation

* Cleanup

* Article reference

* Fix

* Fixes

* Comment out implementation specific tests in Xoshiro_AlgorithmBehavesAsExpected

* Fix

* Reenable sufficient checks for Xoshiro_AlgorithmBehavesAsExpected

* Fix

* Add third party notice

* Resolve comments

* Resolve comments

* Resolve comments

* Resolve comments

* Refactor implementation to separate class

* Typo fix

* stephentoub's refactor

* Reverting NextInt64 on Xoshiro128

* Adjust test

* Update src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro128StarStarImpl.cs

---------

Co-authored-by: Jeff Handley <[email protected]>
Co-authored-by: Stephen Toub <[email protected]>
  • Loading branch information
3 people authored Feb 18, 2023
1 parent 0d04df6 commit 5ec100a
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 141 deletions.
2 changes: 1 addition & 1 deletion THIRD-PARTY-NOTICES.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,7 @@ worldwide. This software is distributed without any warranty.

See <http://creativecommons.org/publicdomain/zero/1.0/>.

License for fastmod (https://github.com/lemire/fastmod) and ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data)
License for fastmod (https://github.com/lemire/fastmod), ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) and fastrange (https://github.com/lemire/fastrange)
--------------------------------------

Copyright 2018 Daniel Lemire
Expand Down
44 changes: 44 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/Random.ImplBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

namespace System
{
public partial class Random
Expand Down Expand Up @@ -29,6 +31,48 @@ internal abstract class ImplBase
public abstract void NextBytes(byte[] buffer);

public abstract void NextBytes(Span<byte> buffer);

// NextUInt32/64 algorithms based on https://arxiv.org/pdf/1805.10941.pdf and https://github.com/lemire/fastrange.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static uint NextUInt32(uint maxValue, XoshiroImpl xoshiro)
{
ulong randomProduct = (ulong)maxValue * xoshiro.NextUInt32();
uint lowPart = (uint)randomProduct;

if (lowPart < maxValue)
{
uint remainder = (0u - maxValue) % maxValue;

while (lowPart < remainder)
{
randomProduct = (ulong)maxValue * xoshiro.NextUInt32();
lowPart = (uint)randomProduct;
}
}

return (uint)(randomProduct >> 32);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static ulong NextUInt64(ulong maxValue, XoshiroImpl xoshiro)
{
UInt128 randomProduct = (UInt128)maxValue * xoshiro.NextUInt64();
ulong lowPart = (ulong)randomProduct;

if (lowPart < maxValue)
{
ulong remainder = (0ul - maxValue) % maxValue;

while (lowPart < remainder)
{
randomProduct = (UInt128)maxValue * xoshiro.NextUInt64();
lowPart = (ulong)randomProduct;
}
}

return (ulong)(randomProduct >> 64);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,46 +90,16 @@ public override int Next()

public override int Next(int maxValue)
{
if (maxValue > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling((uint)maxValue);
while (true)
{
uint result = NextUInt32() >> (sizeof(uint) * 8 - bits);
if (result < (uint)maxValue)
{
return (int)result;
}
}
}
Debug.Assert(maxValue >= 0);

Debug.Assert(maxValue == 0 || maxValue == 1);
return 0;
return (int)NextUInt32((uint)maxValue, this);
}

public override int Next(int minValue, int maxValue)
{
uint exclusiveRange = (uint)(maxValue - minValue);

if (exclusiveRange > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling(exclusiveRange);
while (true)
{
uint result = NextUInt32() >> (sizeof(uint) * 8 - bits);
if (result < exclusiveRange)
{
return (int)result + minValue;
}
}
}
Debug.Assert(minValue <= maxValue);

Debug.Assert(minValue == maxValue || minValue + 1 == maxValue);
return minValue;
return (int)NextUInt32((uint)(maxValue - minValue), this) + minValue;
}

public override long NextInt64()
Expand All @@ -147,6 +117,9 @@ public override long NextInt64()
}
}

// NextInt64 in Xoshiro128 has not been implemented with the fastrange algorithm like the related methods.
// Benchmarking showed that on 32-bit changing implementation could cause regression.

public override long NextInt64(long maxValue)
{
if (maxValue <= int.MaxValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,46 +90,16 @@ public override int Next()

public override int Next(int maxValue)
{
if (maxValue > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling((uint)maxValue);
while (true)
{
ulong result = NextUInt64() >> (sizeof(ulong) * 8 - bits);
if (result < (uint)maxValue)
{
return (int)result;
}
}
}
Debug.Assert(maxValue >= 0);

Debug.Assert(maxValue == 0 || maxValue == 1);
return 0;
return (int)NextUInt32((uint)maxValue, this);
}

public override int Next(int minValue, int maxValue)
{
ulong exclusiveRange = (ulong)((long)maxValue - minValue);

if (exclusiveRange > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling(exclusiveRange);
while (true)
{
ulong result = NextUInt64() >> (sizeof(ulong) * 8 - bits);
if (result < exclusiveRange)
{
return (int)result + minValue;
}
}
}
Debug.Assert(minValue <= maxValue);

Debug.Assert(minValue == maxValue || minValue + 1 == maxValue);
return minValue;
return (int)NextUInt32((uint)(maxValue - minValue), this) + minValue;
}

public override long NextInt64()
Expand All @@ -149,46 +119,16 @@ public override long NextInt64()

public override long NextInt64(long maxValue)
{
if (maxValue > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling((ulong)maxValue);
while (true)
{
ulong result = NextUInt64() >> (sizeof(ulong) * 8 - bits);
if (result < (ulong)maxValue)
{
return (long)result;
}
}
}
Debug.Assert(maxValue >= 0);

Debug.Assert(maxValue == 0 || maxValue == 1);
return 0;
return (long)NextUInt64((ulong)maxValue, this);
}

public override long NextInt64(long minValue, long maxValue)
{
ulong exclusiveRange = (ulong)(maxValue - minValue);

if (exclusiveRange > 1)
{
// Narrow down to the smallest range [0, 2^bits] that contains maxValue.
// Then repeatedly generate a value in that outer range until we get one within the inner range.
int bits = BitOperations.Log2Ceiling(exclusiveRange);
while (true)
{
ulong result = NextUInt64() >> (sizeof(ulong) * 8 - bits);
if (result < exclusiveRange)
{
return (long)result + minValue;
}
}
}
Debug.Assert(minValue <= maxValue);

Debug.Assert(minValue == maxValue || minValue + 1 == maxValue);
return minValue;
return (long)NextUInt64((ulong)(maxValue - minValue), this) + minValue;
}

public override void NextBytes(byte[] buffer) => NextBytes((Span<byte>)buffer);
Expand Down
76 changes: 38 additions & 38 deletions src/libraries/System.Runtime.Extensions/tests/System/Random.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,38 +611,38 @@ public void Xoshiro_AlgorithmBehavesAsExpected()
Assert.Equal(0, randOuter.Next(0));
Assert.Equal(0, randOuter.Next(1));

Assert.Equal(11, randOuter.Next(42));
Assert.Equal(1865324524, randOuter.Next(int.MaxValue));
Assert.Equal(36, randOuter.Next(42));
Assert.Equal(414373255, randOuter.Next(int.MaxValue));

Assert.Equal(0, randOuter.Next(0, 0));
Assert.Equal(1, randOuter.Next(1, 2));
Assert.Equal(12, randOuter.Next(0, 42));
Assert.Equal(7234, randOuter.Next(42, 12345));
Assert.Equal(2147483642, randOuter.Next(int.MaxValue - 5, int.MaxValue));
Assert.Equal(-1236260882, randOuter.Next(int.MinValue, int.MaxValue));
Assert.Equal(8, randOuter.Next(0, 42));
Assert.Equal(4903, randOuter.Next(42, 12345));
Assert.Equal(2147483643, randOuter.Next(int.MaxValue - 5, int.MaxValue));
Assert.Equal(241160533, randOuter.Next(int.MinValue, int.MaxValue));

Assert.Equal(3644728249650840822, randOuter.NextInt64());
Assert.Equal(2809750975933744783, randOuter.NextInt64());
Assert.Equal(7986543274318426717, randOuter.NextInt64());
Assert.Equal(2184762751940478242, randOuter.NextInt64());

Assert.Equal(0, randOuter.NextInt64(0));
Assert.Equal(0, randOuter.NextInt64(1));
Assert.Equal(35, randOuter.NextInt64(42));
Assert.Equal(7986543274318426717, randOuter.NextInt64(long.MaxValue));
Assert.Equal(8, randOuter.NextInt64(42));
Assert.Equal(4799330244130288536, randOuter.NextInt64(long.MaxValue));

Assert.Equal(0, randOuter.NextInt64(0, 0));
Assert.Equal(1, randOuter.NextInt64(1, 2));
Assert.Equal(15, randOuter.NextInt64(0, 42));
Assert.Equal(4155, randOuter.NextInt64(42, 12345));
Assert.Equal(9223372036854775803, randOuter.NextInt64(long.MaxValue - 5, long.MaxValue));
Assert.Equal(375288451405801266, randOuter.NextInt64(long.MinValue, long.MaxValue));
Assert.Equal(29, randOuter.NextInt64(0, 42));
Assert.Equal(9575, randOuter.NextInt64(42, 12345));
Assert.Equal(9223372036854775802, randOuter.NextInt64(long.MaxValue - 5, long.MaxValue));
Assert.Equal(-8248911992647668710, randOuter.NextInt64(long.MinValue, long.MaxValue));

Assert.Equal(0.2885307561293763, randOuter.NextDouble());
Assert.Equal(0.8319616593420064, randOuter.NextDouble());
Assert.Equal(0.694751074593599, randOuter.NextDouble());
Assert.Equal(0.4319359955262648, randOuter.NextDouble());
Assert.Equal(0.00939284326802925, randOuter.NextDouble());
Assert.Equal(0.4631264615107299, randOuter.NextDouble());

Assert.Equal(0.7749006f, randOuter.NextSingle());
Assert.Equal(0.13424736f, randOuter.NextSingle());
Assert.Equal(0.05282557f, randOuter.NextSingle());
Assert.Equal(0.33326554f, randOuter.NextSingle());
Assert.Equal(0.85681933f, randOuter.NextSingle());
Assert.Equal(0.6594592f, randOuter.NextSingle());
}
else
{
Expand Down Expand Up @@ -671,37 +671,37 @@ public void Xoshiro_AlgorithmBehavesAsExpected()
Assert.Equal(0, randOuter.Next(1));

Assert.Equal(23, randOuter.Next(42));
Assert.Equal(1207874445, randOuter.Next(int.MaxValue));
Assert.Equal(1109044164, randOuter.Next(int.MaxValue));

Assert.Equal(0, randOuter.Next(0, 0));
Assert.Equal(1, randOuter.Next(1, 2));
Assert.Equal(33, randOuter.Next(0, 42));
Assert.Equal(2525, randOuter.Next(42, 12345));
Assert.Equal(2147483646, randOuter.Next(int.MaxValue - 5, int.MaxValue));
Assert.Equal(-1841045958, randOuter.Next(int.MinValue, int.MaxValue));
Assert.Equal(2, randOuter.Next(0, 42));
Assert.Equal(528, randOuter.Next(42, 12345));
Assert.Equal(2147483643, randOuter.Next(int.MaxValue - 5, int.MaxValue));
Assert.Equal(-246770113, randOuter.Next(int.MinValue, int.MaxValue));

Assert.Equal(364988307769675967, randOuter.NextInt64());
Assert.Equal(4081751239945971648, randOuter.NextInt64());
Assert.Equal(7961633792735929777, randOuter.NextInt64());
Assert.Equal(1188783949680720902, randOuter.NextInt64());

Assert.Equal(0, randOuter.NextInt64(0));
Assert.Equal(0, randOuter.NextInt64(1));
Assert.Equal(8, randOuter.NextInt64(42));
Assert.Equal(3127675200855610302, randOuter.NextInt64(long.MaxValue));
Assert.Equal(1, randOuter.NextInt64(42));
Assert.Equal(3659990215800279771, randOuter.NextInt64(long.MaxValue));

Assert.Equal(0, randOuter.NextInt64(0, 0));
Assert.Equal(1, randOuter.NextInt64(1, 2));
Assert.Equal(25, randOuter.NextInt64(0, 42));
Assert.Equal(593, randOuter.NextInt64(42, 12345));
Assert.Equal(5, randOuter.NextInt64(0, 42));
Assert.Equal(9391, randOuter.NextInt64(42, 12345));
Assert.Equal(9223372036854775805, randOuter.NextInt64(long.MaxValue - 5, long.MaxValue));
Assert.Equal(-1415073976784572606, randOuter.NextInt64(long.MinValue, long.MaxValue));
Assert.Equal(7588547406678852723, randOuter.NextInt64(long.MinValue, long.MaxValue));

Assert.Equal(0.054582986776168796, randOuter.NextDouble());
Assert.Equal(0.7599686772523376, randOuter.NextDouble());
Assert.Equal(0.9113759792165226, randOuter.NextDouble());
Assert.Equal(0.3010761548802774, randOuter.NextDouble());
Assert.Equal(0.5866389350236931, randOuter.NextDouble());
Assert.Equal(0.4726054469222304, randOuter.NextDouble());

Assert.Equal(0.3010761f, randOuter.NextSingle());
Assert.Equal(0.8162224f, randOuter.NextSingle());
Assert.Equal(0.5866389f, randOuter.NextSingle());
Assert.Equal(0.35996222f, randOuter.NextSingle());
Assert.Equal(0.929421f, randOuter.NextSingle());
Assert.Equal(0.5790618f, randOuter.NextSingle());
}
}

Expand Down

1 comment on commit 5ec100a

@Xyncgas
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to also add Vector acceleration to this rng (System.Numeric)

Please sign in to comment.