When random isn't random enough: the modulo trap
Translated by a model; the original is in Russian.
There's a pattern found in codebases everywhere: given a set of objects, pick one at random, each with equal odds. The direct solution is to grab a big random number and clamp it to the number of choices with modulo: choices[random_u64() % 10]. The author admits to writing exactly this many times, especially in C, whose standard library offers nothing beyond rand().
The trouble is that this code doesn't preserve the uniformity of the underlying random function. We assume each of ten objects has a 1/10 chance. It doesn't.
Why modulo skews the distribution
Suppose we draw a number from [0, 9] and pick one of three objects with % 3. Just tabulate the outcomes: {0, 3, 6, 9} give object one, {1, 4, 7} give object two, {2, 5, 8} give object three. Four inputs against three: the first option comes up 40% of the time instead of the intended 33%, while the other two land at just 30%.
It's one of the classic blunders, especially when random_u64() is all you have. The correct tool, the author says, is a random_between(l, h) that draws uniformly from a given inclusive range.
Spell out the distribution instead
In the author's view, random_u64() is too low-level. In practice you rarely want a bare number: you want to pick an object, and sometimes to weigh some choices more than others. So the better default is random_choice(), where you hand it the options and their probabilities yourself. In that form the erroneous skew sticks out like a sore thumb: if the first option reads 0.4 and the rest 0.3, something is clearly off.
To dodge floating-point instability and the nuisance of making weights sum to exactly 1.0, many standard libraries (Python among them) use relative integer weights. Weights (4, 3, 3) sum to 10, and 3/10 = 0.3 as expected. The implementation is simple: with weights 15 + 12 + 3 = 30, draw a number in [0, 29] and map it onto sub-intervals — another sign that random_between belongs in the toolkit.
Working in the wrong mental domain
Deeper down lies a modeling mistake: we're dealing not with numbers but with random variables. Non-linear operators don't preserve expected value — f(E[X]) is not always E[f(X)]. We think we're preserving a property of a particular value when we actually need to preserve the properties of the function itself.
The practical takeaway
The author's team are customers of Antithesis, a deterministic fuzzing platform that exercises important code paths at random. There it's handy to state distributions directly: tag Small uploads with weight 88 and three larger variants with 4 each, so roughly ⅞ of tests hit small blobs. On review, they removed every random_u64() call, replacing them with weighted random_choice(), and placed a moratorium on new ones. Next time you see code like the original sample, ask yourself: can I just express the distribution I want directly?
Source: Lobsters
Comments
Sign in, to comment.