Repository navigation
Potential 1.5x performance improvement for Random.Shuffle() #82838
Description
Activity
- addedtenet-performancePerformance related issuePerformance related issue
on Mar 1, 2023 - ghost addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Mar 1, 2023 the results of shuffling with the same seed would be different (but still random!)
Presumably, it's fine to change the algorithm when seed is not specified by user, e.g.
new Random()orRandom.SharedThis has overlap with #82286; both are effectively looking at reducing the number of random numbers generated as part of an operation that may need multiple generated. You might coordinate with @sakno on a single approach to try for both GetItems and Shuffle.
I am curious, though, regarding the approach outlined here, whether it would preserve the quality / properties of the random numbers being generated. We don't, for example, just use % as part of Next(int), because it skews the distribution inappropriately (in addition to the cost involved), and will instead use retries or more recently the fastrange algorithm.
Reacted by Roman Sakno%can be replaced with&only if the length ofchoicesis a power of 2, if we want to have unbiased selection of the items. This is why I choseNextBytesto pre-populate vector of random values instead of%when length is not a power of 2. Anyway, fastrange is still applied to that vector. Also, this approach produces the same result for the same seed.This has overlap with #82286; both are effectively looking at reducing the number of random numbers generated as part of an operation that may need multiple generated. You might coordinate with @sakno on a single approach to try for both GetItems and Shuffle.
Thanks for the suggestion.
GetItemsis quite different fromShuffle(it's sampling the same uniform distribution repeatedly and the results are independent so it can be parallelized) so there probably isn't much potential for direct code reuse. That said, the type of optimization being suggested in that PR could potentially be applied here. I am ultimately committing the same sin of generating 64 bit integers and then throwing away half the bits. Unfortunately everything I've tried (including @sakno 's code) to alleviate that problem has ended up making it a lot slower, probably because I'm already massively reducing the number of bits I need to generate. I could easily have missed something though.I am curious, though, regarding the approach outlined here, whether it would preserve the quality / properties of the random numbers being generated. We don't, for example, just use % as part of Next(int), because it skews the distribution inappropriately (in addition to the cost involved), and will instead use retries or more recently the fastrange algorithm.
This approach is completely unbiased (assuming the underlying prng is).
I'm essentially using the existingNextmethod (using the single argument form where you provide an upper bound) to generate a random element of the symmetric group (essentially the set of possible permutations) and then applying that permutation to the array so every possible result is equally likely.I'm essentially using the existing Next method (using the single argument form where you provide an upper bound) to generate a random element of the symmetric group
I must be misunderstanding your suggestion then.
Please feel free to put up a PR with your suggested change and we can collectively evaluate it with the exact code in front of us.
Please feel free to put up a PR with your suggested change and we can collectively evaluate it with the exact code in front of us.
I am working on this. I've just had a few personal issues come up in the last week...
- ghost addedin-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is merged
on Mar 11, 2023 - ghost removedin-prThere is an active PR which will close this issue when it is mergedThere is an active PR which will close this issue when it is merged
on Jul 6, 2023 Tried and decided against in #83305
- ghost removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jul 6, 2023 - ghost locked as resolved and limited conversation to collaborators
on Aug 5, 2023
Description
Hi. This is not a performance problem per se but a potential for quite a big improvement.
I noticed that a
Shuffle()method has been added to theRandomclass.I recently did some performance optimizations in the rust
randcrate (library) which made it run about 1.5x times faster . I've had a little go and gotten similar results in C# (see benchmarks below).How it works
Shuffling an array of length
nessentially involves makingnrandom swaps between elements.The way I've made it faster is to, instead of generating a new 32 bit random number for each swap, group the swaps together into the largest groups possible and only generate one random number per group. (I'm simplifying - counting calls to
Random.Next(n)here which themselves may call_prng.Sample()more than once)For an array of length 12, instead of generating 12 different random numbers, you generate one random number in the range [0,479001600) and use div and mod methods to work out which swaps to do. For longer arrays you use more groups and they get a bit smaller but the end result is a lot fewer calls to the comparatively slow
Next(n)function.Data
These are my benchmark results, testing the existing "Old" implementation vs my proposed "New" implementation for int arrays of length 10, 100, and 1000
The new versions are all about 1.5x as fast as their equivalents.
Configuration
dotnet 8.0.100-preview.1.23115.2
Windows 11 Pro 22000.1574
x64
12th Gen Intel(R) Core(TM) i7-12700H 2.30 GHz
Breaking Changes
This change would not alter the API in any way but would be a value breaking change - the results of shuffling with the same seed would be different (but still random!) and the prng would almost always be advanced fewer steps. For this reason, if this is considered worth doing, I suggest doing it sooner rather than later to avoid annoying people who are reliant on value stability.
Pull Request
I'm happy to tidy up my code and make a pull request for this if it's considered worthwhile.