Skip to content

Share one RandomNumberGenerator and draw random bytes in batches - #703

Merged
SimonCropp merged 1 commit into
mainfrom
perf/rng-shared-instance
Oct 12, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
perf/rng-shared-instance

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

The RandomNumberGenerator polyfills created and disposed a RandomNumberGenerator for every call, and GetItems, GetString and Shuffle did that once per element through GetInt32 (GetString(62 choices, 32) allocated 16.5 KB on net48).

  • One shared instance (targets without the static Fill: net4x, netstandard2.0, netcoreapp2.0). RandomNumberGenerator instances are thread safe, so GetInt32, GetBytes and Fill use one static instance, created on first use.
  • GetInt32 reads its four bytes into a thread-static buffer (cleared after use) instead of a new byte[4] per call. The masked rejection sampling is unchanged.
  • Fill(Span<byte>) goes through a pooled buffer in chunks of up to 4096 bytes instead of a temporary array the size of the destination. The buffer is cleared before it goes back to the pool. (GetBytes(byte[], int, int) is not used: on .NET Framework it allocates an array of its own.)
  • GetItems / GetString / Shuffle (everything below net8) draw their random bytes from a 256 byte stack buffer that is refilled when used up, rather than making one call to the generator per element. Each value is still produced by the same masked rejection sampling over four fresh random bytes, so the distribution is the same and stays unbiased; only the number of calls to the generator changes.
  • GetString builds strings of up to 256 chars on the stack instead of in a char[].

GetHexString is not changed, but gets cheaper on net4x through Fill and GetBytes.

Not included: Random.NextBytes(Span<byte>) (that is in the Random PR).

Tests

  • GetInt32: uniform over a range that is not a power of two (30,000 draws, each of 3 values within 9,000..11,000), and correct from 8 threads at once
  • Fill: lengths 0, 1, 31, 4095, 4096, 4097 and 100,000 into the middle of a larger array: nothing outside the span is written, every chunk including the tail is filled; and from 8 threads at once
  • GetItems: uniform for 1, 2, 3, 62, 256 and 300 choices (400 draws per choice, each count within 250..550), lengths around the 64 element buffer boundary, empty destination, empty choices throw
  • GetString: lengths 0, 1, 32, 256, 257 and 5000 use only the choices, and differ between calls
  • Shuffle: a permutation for lengths 0, 1, 2, 63, 64, 65 and 2000, and all 6 orders of 3 elements come up evenly over 6,000 shuffles
  • GetBytes and GetHexString(int) at several lengths

The bounds on the statistical checks are more than 6 standard deviations wide. The new tests pass against the previous implementation and against the BCL on net8+.

Measurements

Stopwatch / GC.GetAllocatedBytesForCurrentThread, best of 9 rounds after warm-up, Release. The machine was busy, so treat the times as indicative; the allocation figures are exact.

net48

before after
GetInt32(100) 683 ns / 512 B 159 ns / 0 B
GetInt32(10, 1000) 653 ns / 512 B 138 ns / 0 B
GetBytes(32) 693 ns / 536 B 128 ns / 56 B
Fill(32B) 706 ns / 536 B 161 ns / 0 B
Fill(64KB) 22,673 ns / 66,056 B 14,426 ns / 0 B
GetItems(62 choices, 32 dest) 16,655 ns / 16,384 B 898 ns / 0 B
GetItems(62 choices, 32) 16,722 ns / 16,472 B 1,304 ns / 88 B
GetString(62 choices, 32) 16,911 ns / 16,568 B 983 ns / 96 B
Shuffle(32 ints) 17,338 ns / 15,872 B 1,260 ns / 0 B
GetHexString(32) 589 ns / 704 B 421 ns / 224 B
GetHexString(32 dest) 702 ns / 520 B 321 ns / 0 B

net6.0 (GetInt32, GetBytes and Fill are the BCL's there)

before after
GetItems(62 choices, 32 dest) 1,660 ns / 0 B 399 ns / 0 B
GetItems(62 choices, 32) 1,661 ns / 88 B 390 ns / 88 B
GetString(62 choices, 32) 2,067 ns / 176 B 384 ns / 88 B
Shuffle(32 ints) 2,145 ns / 0 B 529 ns / 0 B

@SimonCropp SimonCropp modified the milestones: 11.4.4, 11.4.5 Oct 12, 2026
@SimonCropp
SimonCropp merged commit f776f4c into main Oct 12, 2026
7 checks passed
@SimonCropp
SimonCropp deleted the perf/rng-shared-instance branch October 12, 2026 12:10
This was referenced Oct 12, 2026
This was referenced Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant