Implementation of Lemire's nearly divisionless method (#79790)
authorMorten Larsen <mla@specialisterne.com>
Sat, 18 Feb 2023 03:00:06 +0000 (04:00 +0100)
committerGitHub <noreply@github.com>
Sat, 18 Feb 2023 03:00:06 +0000 (22:00 -0500)
* 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 <jeffhandley@users.noreply.github.com>
Co-authored-by: Stephen Toub <stoub@microsoft.com>
THIRD-PARTY-NOTICES.TXT
src/libraries/System.Private.CoreLib/src/System/Random.ImplBase.cs
src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro128StarStarImpl.cs
src/libraries/System.Private.CoreLib/src/System/Random.Xoshiro256StarStarImpl.cs
src/libraries/System.Runtime.Extensions/tests/System/Random.cs

index 54fca29..feb4d4f 100644 (file)
@@ -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
index 055595c..6bd1b8a 100644 (file)
@@ -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
@@ -29,6 +31,48 @@ namespace System
             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);
+            }
         }
     }
 }
index cfd33be..305ce4a 100644 (file)
@@ -90,46 +90,16 @@ namespace System
 
             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()
@@ -147,6 +117,9 @@ namespace System
                 }
             }
 
+            // 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)
index bb36417..2b05a77 100644 (file)
@@ -90,46 +90,16 @@ namespace System
 
             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()
@@ -149,46 +119,16 @@ namespace System
 
             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);
index 706e19b..0903c99 100644 (file)
@@ -611,38 +611,38 @@ namespace System.Tests
                 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
             {
@@ -671,37 +671,37 @@ namespace System.Tests
                 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());
             }
         }