Fast non-cryptographic PRNG NIF for Erlang. One function: knot:uniform/1.

wyrand (1 multiply + 1 xor + 1 add per draw; passes BigCrush), bounded via biased multiply-and-shift (bias < 1e-7 for the small bounds shackle uses). State is per OS thread via __thread — no locks on the hot path. Lazy seeding from getrandom/arc4random_buf on first use.

A drop-in replacement for granderl:uniform/1 that builds cleanly on modern OTP and scales linearly across schedulers.

API

-spec knot:uniform(pos_integer()) -> pos_integer().

uniform(N) returns a uniformly random integer in [1, N]. N must fit in a u32 (1..=4_294_967_295).

Install

{deps, [{knot, "0.1.0"}]}.

Requires a C compiler (cc) on the build host — universally available on systems that already run Erlang. No Rust toolchain, no cargo, no extra deps.

Build

rebar3 compile runs c_src/build.sh, which:

  • Resolves ERTS_INCLUDE_DIR via erl -noshell -eval ... -s init stop (option order is correct for OTP 27+ — the bug that affected granderl 0.1.5 is fixed here).
  • Compiles c_src/knot.c with -O3 -march=native -mtune=native.
  • Outputs priv/knot.so.

Env vars honored:

VarEffect
ERTS_INCLUDE_DIRSkip the erl probe; use this path for erl_nif.h.
CCCompiler (default cc).
CFLAGSExtra flags appended after defaults.
KNOT_NO_NATIVEIf set, omit -march=native/-mtune=native (use for portable cross-platform builds).

Benchmark

Apple Silicon (M-series), OTP 29, 10M iterations of uniform(254), median of 5 runs:

concurrencyrand:uniform/1granderl:uniform/1knot:uniform/1
134 ns/op13 ns/op12 ns/op
88 ns/op7 ns/op3.3 ns/op
326 ns/op8 ns/op3.2 ns/op
1286 ns/op8.5 ns/op3.1 ns/op

Single-process: dispatch-bound (~12 ns is the NIF boundary floor for both knot and granderl).

Concurrent: knot scales linearly across schedulers because state is strictly per-OS-thread (__thread uint64_t), no atomics or locks anywhere on the hot path.

Reproduce: make bench.

License

MIT.