I am in need of a "debiased" clamp function, to generate a uniformly-distributed random number from a good PRNG.
Let's assume that RNG
in the below code is a field containing an instance of a good (or good enough) PRNG such as PCG, Xoshiro, or MT, which produced a reasonably-uniform distribution of random integers.
public int GetClamped(int maxnum) {
// maxnum is totally arbitrary here, and likely not a power of 2
int cap = int.MaxValue / maxnum * maxnum;
int r;
do {
r = RNG.NextPositiveInt(); // Returns a value in the range [0, int.MaxValue]
} while (r >= cap);
return r >= maxnum ? r % maxnum : r;
}
I wonder if this can be optimized further, seeing I have 2 divisions and a multiplication there.
About "Debiasing"
Let's say a good PRNG returns a 16-bit value in the range of [0, 65535]
with a uniform distribution.
If I clamp the value to 1000
and simply use the modulus %
operator, the values in the range of [0, 535]
after clamping will appear one time more often than the values in the range of [536, 999]
(because there is no 65536, 65537 ... 65999
)
This means that a simple clamping using the modulus %
operator introduces a bias towards the lower range.
The algorithm above tries to find the largest value which will still satisfy the uniform distribution (64999
) and discards all values above that cap to pull a new value from the PRNG.
The principle is that if [0, 65535]
is uniform, then a truncated range of [0, 64999]
will still be uniform. Hence the algorithm name of "unbiased clamping" or "debiased clamping".
Do note that the clamping value maxnum
is arbitrary; it is not necessarily 1000
(like in this example), but can be any value in accordance to the user's needs. So, a precalculated table of "multiplicative equivalent to division" is simply not practical.
return RNG.NextPositiveInt() % maxnum
? \$\endgroup\$return (int)((double)maxnum / int.MaxValue * RNG.NextPositiveInt())
or you may initially generatedouble
to reduce conversion complexity. Anyway this one faster than any of the optimistic scenarios applied to the initial solution. \$\endgroup\$0.1
-- and integer multiples of -- cannot be represented in floating point accurately). This greatly complicates trying to prove that the scaling does not cause a bias, unlike the simple axiom of "truncating a uniform dist will result in a still-uniform dist". In short, I'd like to stay in the integer land when possible. \$\endgroup\$