Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Lemire's nearly divisionless method #79790

Merged
merged 32 commits into from
Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c5dadb4
Lemire implementation
mla-alm Dec 15, 2022
71b3bb3
Cleanup
mla-alm Dec 17, 2022
405c976
Article reference
mla-alm Dec 17, 2022
a3c263f
Fix
mla-alm Dec 17, 2022
96b5e91
Fixes
mla-alm Dec 17, 2022
600b9d5
Comment out implementation specific tests in Xoshiro_AlgorithmBehaves…
mla-alm Dec 18, 2022
e5c4536
Fix
mla-alm Dec 18, 2022
8a7dbdf
Merge remote-tracking branch 'origin/main' into mla-alm/lemire
mla-alm Dec 18, 2022
2dd1a78
Reenable sufficient checks for Xoshiro_AlgorithmBehavesAsExpected
mla-alm Dec 21, 2022
1b3c19b
Merge remote-tracking branch 'origin/main' into mla-alm/lemire
mla-alm Dec 21, 2022
f2a11d0
Fix
mla-alm Dec 21, 2022
a68e011
Add third party notice
mla-alm Dec 22, 2022
4ae4914
Resolve comments
mla-alm Dec 23, 2022
a4a6f93
Resolve comments
mla-alm Dec 23, 2022
e380d47
Resolve comments
mla-alm Dec 23, 2022
f50eabc
Merge remote-tracking branch 'origin/main' into mla-alm/lemire
mla-alm Dec 24, 2022
c739482
Merge branch 'main' into mla-alm/lemire
jeffhandley Jan 26, 2023
1bec03e
Resolve comments
mla-alm Jan 28, 2023
6d7b1f0
Merge branch 'dotnet:main' into mla-alm/lemire
mla-alm Jan 28, 2023
0cc3ecd
Refactor implementation to separate class
mla-alm Jan 28, 2023
20364ec
Merge remote-tracking branch 'origin/mla-alm/lemire' into mla-alm/lemire
mla-alm Jan 28, 2023
a22e745
Merge branch 'dotnet:main' into mla-alm/lemire
mla-alm Jan 29, 2023
9802461
Typo fix
mla-alm Jan 29, 2023
cc32925
Merge branch 'dotnet:main' into mla-alm/lemire
mla-alm Jan 30, 2023
89cb3bd
stephentoub's refactor
mla-alm Jan 30, 2023
4370688
Merge remote-tracking branch 'origin/mla-alm/lemire' into mla-alm/lemire
mla-alm Jan 30, 2023
ef38e9d
Reverting NextInt64 on Xoshiro128
mla-alm Feb 16, 2023
33654d2
Merge branch 'dotnet:main' into mla-alm/lemire
mla-alm Feb 16, 2023
95b4b42
Adjust test
mla-alm Feb 16, 2023
b28062c
Merge remote-tracking branch 'origin/mla-alm/lemire' into mla-alm/lemire
mla-alm Feb 16, 2023
6304f68
Merge branch 'dotnet:main' into mla-alm/lemire
mla-alm Feb 17, 2023
608a801
Update src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro…
stephentoub Feb 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
stephentoub marked this conversation as resolved.
Show resolved Hide resolved

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);
danmoseley marked this conversation as resolved.
Show resolved Hide resolved

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)
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
{
// 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));
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
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