0
\$\begingroup\$

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.

\$\endgroup\$
10
  • \$\begingroup\$ Why not simply return RNG.NextPositiveInt() % maxnum? \$\endgroup\$
    – aepot
    Commented Jun 11, 2021 at 17:21
  • 1
    \$\begingroup\$ @aepot That will cause a biased output; the lower values will appear more often than the higher values. \$\endgroup\$
    – pepoluan
    Commented Jun 11, 2021 at 18:54
  • \$\begingroup\$ Ok, return (int)((double)maxnum / int.MaxValue * RNG.NextPositiveInt()) or you may initially generate double to reduce conversion complexity. Anyway this one faster than any of the optimistic scenarios applied to the initial solution. \$\endgroup\$
    – aepot
    Commented Jun 11, 2021 at 18:58
  • \$\begingroup\$ @aepot Although in theory that should work (scaling the range), in practice performing the floating point division will result in numbers 'not accurately representable' in floating point (e.g., 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\$
    – pepoluan
    Commented Jun 11, 2021 at 19:19
  • \$\begingroup\$ Ok, this makes some noise to RNG making state-based predictability somewhat more complicated (RNG security improvement?). Btw, you're not doing exact computations but random numbers generation. How much CPU time do you want to pay to avoid that noise? What it may affect? Here's no sense to stay in integers, it doesn't worth to introduce polling loop instead of simple FP division+multiplication. Or I'm missing something. \$\endgroup\$
    – aepot
    Commented Jun 11, 2021 at 19:28

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.