Document random options (#55)

This commit is contained in:
Lars Müller 2023-12-23 23:49:17 +01:00 committed by GitHub
parent 9a0a50506c
commit cf4192e8ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

193
doc/random.adoc Normal file

@ -0,0 +1,193 @@
= Random
include::../include/config.adoc[]
include::../include/types.adoc[]
:description: Comparison of available random number generators ("sources")
:keywords: random, source, secure
Minetest provides four different random sources, each with its own merits.
Modders must choose wisely unless they can let the engine do the random for them
(e.g. randomly picking a sound or a texture for particles).
== Lua builtins
Not restricted by mod security, these functions are available to both SSMs and CSMs:
=== https://www.lua.org/manual/5.1/manual.html#pdf-math.randomseed[`math.randomseed`]
Seed the random. Minetest already does this for you using the system time.
[IMPORTANT]
.Do not seed the random
====
Do not seed the random to turn it into a deterministic random source
as other mods may expect it to be "non-deterministic".
Conversely, do not rely on the random to have any particular seed either; other mods & the engine
may have seeded it (using the system time) to be "non-deterministic".
The problem with `math.randomseed` is that there is only one global, hidden seed.
There is no way to get the current seed out; mods can't restore their random sequence.
Mods seeding the random thus necessarily conflict - unless they all expect
it to be "non-deterministic" and only seed it accordingly
(ideally not at all, since the engine-side seeding should suffice).
If you need `math.random` for its performance but want it to be deterministic,
you may *reseed* the random after you're done with it to ensure that it is "non-deterministic" again.
[source,lua]
---
-- Use the random to generate a seed for the random; preferable over using system time,
-- as the latter may be deterministic
local seed = ... -- some fixed seed
local reseed = math.random(2^31-1)
math.randomseed(seed) -- temporarily make the random "deterministic"
-- ... do something using `math.random` ...
math.randomseed(reseed)
---
====
=== https://www.lua.org/manual/5.1/manual.html#pdf-math.random[`math.random`]
Get a random number. Very versatile; allows getting floats between `0` and `1` or integers in a range.
NOTE: The random numbers between `0` and `1` do not provide a full 52-bit mantissa
full of entropy; they usually have around 32 bits of entropy.
WARNING: When using this to obtain integers,
make sure that both the upper & lower bound
as well as their difference are within the C `int` range -
otherwise you may get overflows & errors.
TIP: Use `math.random` as your go-to versatile "non-deterministic" random source.
== Random Number Generators
=== `PcgRandom`
A seedable 32-bit signed integer pseudo-random number generator.
==== `PcgRandom(seed)`
Constructs a `PcgRandom` instance with the given seed,
which should be an integer within 32-bit bounds.
==== Methods
===== `:next([min, max])`
If `min` and `max` are both omitted,
they default to `-2^31` (`-2147483648`)
and `2^31 - 1` (`2147483647`) respectively.
===== `:rand_normal_dist(min, max, [num_trials])`
WARNING: No successful use of this function is documented. Consider implementing your own normal distribution instead.
`min` and `max` are required; they need to be integers.
Rough approximation of a normal distribution with a mean of `(max - min) / 2`
and a variance of `(((max - min + 1) ^ 2) - 1) / (12 * num_trials)`.
`num_trials` defaults to `6`. The more trials, the better the approximation.
The return value is a float.
=== `PseudoRandom`
A seedable 16-bit unsigned integer pseudo-random number generator.
"Uses a well-known LCG algorithm introduced by K&R."
Perhaps the lowest-quality random generator of all.
==== `PseudoRandom(seed)`
Constructor: Takes a `seed` and returns a `PseudoRandom` object.
===== `:next([min, max])`
If `min` and `max` are both omitted, they default to `0` and `2^16-1` (`32767`) respectively.
WARNING: Requires `((max - min) == 32767) or ((max-min) <= 6553))` for a proper distribution.
=== `SecureRandom`
System-provided cryptographically secure random:
An attacker should not be able to predict the generated sequence of random numbers.
Use this when generating cryptographic keys or tokens.
Not necessarily available in all builds and on all platforms.
==== `SecureRandom()`
Constructor: Returns a SecureRandom object or `nil` if no secure random source is available.
TIP: Use `assert(SecureRandom(), "no secure random available")` to error if no secure random source is available.
==== Methods
===== `:next_bytes([count])`
Only argument is `count`, an optional integer defaulting to `1`
and limited to `2048` specifying how many bytes are to be returned.
Returned as a Lua bytestring of length `count`
== Benchmarking
[source,lua]
---
collectgarbage"stop" -- we don't want GC heuristics to interfere
local n = 1e8 -- number of runs
local function bench(name, constructor, invokation)
local func = assert(loadstring(([[
local r = %s
for _ = 1, %d do %s end
]]):format(constructor, n, invokation)))
local t = minetest.get_us_time()
func()
print(name, (minetest.get_us_time() - t) / n, "µs/call")
end
bench("Lua", "nil", "math.random()")
bench("PCG", "PcgRandom(42)", "r:next()")
bench("K&R", "PseudoRandom(42)", "r:next()")
bench("Secure", "assert(SecureRandom())", "r:next_bytes()")
---
Example output:
[source]
---
Lua 0.00385002 µs/call
PCG 0.05579729 µs/call
K&R 0.05859349 µs/call
Secure 0.11211887 µs/call
---
== Comparison
|===
| Random Source | Performance | Bytes of entropy | Seedability | Versatility | Distribution | Security | Portability
| `math.random` | very good (1x) | up to 4 | global seed; seeded by default | very good | no guarantees, but usually decent enough | not cryptographically secure | varies by platform
| `PcgRandom` | okay (~14x) | up to 4 | per-instance seed | very good | good, decent guarantees | not cryptographically secure | always the same
| `PseudoRandom` | okay (~15x) | 1 to 2 | per-instance seed | outright sucks | okay-ish | not cryptographically secure | always the same
| `SecureRandom` | still okay (30x) | 1 to 2048 | not seedable | cryptographically secure | varies by platform; may be missing
|===
Note: The performance comparison is a bit of an apples-to-oranges comparison for multiple reasons:
. The different generators make different guarantees regarding the randomness;
. The different generators generate different numbers of bytes per invocation - the default was arbitrarily chosen;
Secure random in particular is able to generate plenty of bytes (up to 2048) with one call.
The benchmark still suffices to draw basic conclusions though,
especially for the common case where a random source is simply used once
(e.g. `math.random() < 0.5`).
=== Conclusion
. *Never use `PseudoRandom`. It is strictly inferior to `PcgRandom`.*
. Use `math.random` if you want a fast "non-deterministic" random.
. Use `PcgRandom` if you need per-instance seedability and can take the performance hit.
. Use `SecureRandom` if and only if you need a cryptographically secure random.