Perform 32-bit random if the bound fits into 32 bits at runtime.
Patch: random32.patch.
Motivating example:
{$mode objfpc}
uses
Math;
var
shuffleBag: array of int32;
i: SizeInt;
begin
write('Shuffle: ');
shuffleBag := [1, 2, 3, 4, 5, 6, 7];
repeat
i := random(length(shuffleBag));
write(shuffleBag[i], ' ');
delete(shuffleBag, i, 1);
until length(shuffleBag) = 0;
writeln(LineEnding, 'Nefia boss hex resistance = ', RandomRange(0, 100), '%');
end.
Output on x86-32
before the patch (and everywhere after the patch):
Shuffle: 7 4 5 6 2 1 3
Nefia boss hex resistance = 64%
Output on x86-64
before the patch:
Shuffle: 7 5 2 6 3 4 1
Nefia boss hex resistance = 90%
“Shuffle” part might be understandable: length()
is a SizeInt
, which is an int32
on 32 bits and int64
on 64 bits, so they call different random
overloads which work differently in general and draw different amounts of uint32
from the underlying generator, so what’d you expect. However, this is very inconvenient if you seek reproducibility across bitnesses, and random(int32(length()))
is an extremely annoying workaround: in addition to being cumbersome, it, well, truncates length()
to 32 bits which means all of its instances have the potential to become hard-to-find bugs.
Moreover, int64
is a native type on 64 bits, so calculating the random
bound in place (a, b: int32; random(a + b)
) will often upcast it to 64 bits according to FPC rules, leading to more unobvious results, like bitness-dependent Math.RandomRange
which does exactly that under the hood.
I personally did this change with my own RNG a thousand years ago, and remembered about the same problem with standard System.Random
because I found this issue while searching for something unrelated. It had a completely different complaint which is not fixed by my patch, but I still think it can be closed because it’s long out of date (and the complaint would remain DUBIOUS regardless of that, like, the only reason to do that is validating the underlying generator, which is not going to lose much of the reliability from checking every other value).
As you might suspect from having a glimpse at random(int32)
and random(int64)
implementations, this is also an optimization to random(int64)
: on my computer, it becomes 50–80% faster (not inlining random(int32)
reduces this to 30–50%) when its argument fits into int32
, at the cost of 5–10% slowdown when it doesn’t.