From 1a3c105a8b919d5c1d0f51e60aa7f539e620f2d3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 19 Sep 2024 08:26:40 -0400 Subject: [PATCH] Fix Random.GetItems observably breaking change in produced sequence Developers, often in tests, rely on seeded Random instances producing the same sequence of values on every use. We made a change in .NET 9, though, that changed the sequence GetItems produces, due to employing a different algorithm. This fixes that special-case to only be used when the developer couldn't rely on the results being deterministic, namely when using either `new Random()` or `Random.Shared`. If a seed is provided or if a custom derived implementation is used, it falls back to the old behavior. --- src/libraries/System.Private.CoreLib/src/System/Random.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Random.cs b/src/libraries/System.Private.CoreLib/src/System/Random.cs index 190fa3583caac..39438be55c8f0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Random.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Random.cs @@ -199,11 +199,16 @@ public void GetItems(ReadOnlySpan choices, Span destination) // The most expensive part of this operation is the call to get random data. We can // do so potentially many fewer times if: + // - the instance was constructed as `new Random()` or is `Random.Shared`, such that it's not seeded nor is it + // a custom derived type. We don't want to observably change the deterministically-produced sequence from previous releases. // - the number of choices is <= 256. This let's us get a single byte per choice. // - the number of choices is a power of two. This let's us use a byte and simply mask off // unnecessary bits cheaply rather than needing to use rejection sampling. // In such a case, we can grab a bunch of random bytes in one call. - if (BitOperations.IsPow2(choices.Length) && choices.Length <= 256) + ImplBase impl = _impl; + if ((impl is null || impl.GetType() == typeof(XoshiroImpl)) && + BitOperations.IsPow2(choices.Length) && + choices.Length <= 256) { Span randomBytes = stackalloc byte[512]; // arbitrary size, a balance between stack consumed and number of random calls required while (!destination.IsEmpty)