Created
April 20, 2026 18:35
-
-
Save helabenkhalfallah/78e52661297498e863ea53f3a1f6b33c to your computer and use it in GitHub Desktop.
What Each Random Call Actually Does?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // benchmark-random.mjs | |
| // | |
| // Reproducible benchmark for JavaScript random APIs. | |
| // Uses tinybench for proper statistics (median, p75, p99, relative margin of error). | |
| // | |
| // Usage: | |
| // npm install tinybench | |
| // node benchmark-random.mjs | |
| // | |
| // Notes on interpretation: | |
| // - Absolute numbers depend on CPU, V8 version, and background load. | |
| // Ratios between tasks are substantially more stable than absolute ns. | |
| // - Microbenchmark harnesses add a fixed per-call overhead (nanoseconds). | |
| // Treat values below ~50 ns as "effectively free" rather than precise. | |
| // - Run with CPU frequency scaling disabled and all other work idle for | |
| // numbers closer to steady-state truth. | |
| import { Bench } from 'tinybench'; | |
| import { randomUUID, randomInt, randomBytes, getRandomValues } from 'node:crypto'; | |
| import os from 'node:os'; | |
| // Defeat dead-code elimination: every task assigns to a global so V8 | |
| // cannot prove the work is unused and optimize it away. | |
| globalThis.__sink = 0; | |
| // --- shared buffers so allocation cost does not pollute the measurements --- | |
| const singleBuf = new Uint32Array(1); | |
| const BATCH = 1024; | |
| const batchBuf = new Uint32Array(BATCH); | |
| let batchIdx = BATCH; | |
| function nextBatchedUint32() { | |
| if (batchIdx >= BATCH) { | |
| getRandomValues(batchBuf); | |
| batchIdx = 0; | |
| } | |
| return batchBuf[batchIdx++]; | |
| } | |
| function fisherYates10() { | |
| const a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | |
| for (let k = 9; k > 0; k--) { | |
| const i = Math.floor(Math.random() * (k + 1)); | |
| const tmp = a[k]; a[k] = a[i]; a[i] = tmp; | |
| } | |
| return a; | |
| } | |
| function unbiasedIntCrypto(N) { | |
| const cutoff = 2 ** 32 - (2 ** 32 % N); | |
| const b = new Uint32Array(1); | |
| while (true) { | |
| getRandomValues(b); | |
| if (b[0] < cutoff) return b[0] % N; | |
| } | |
| } | |
| // --- the bench --- | |
| const bench = new Bench({ | |
| time: 1000, // 1 s of measurement per task | |
| warmupTime: 200, // 200 ms of warmup per task | |
| warmupIterations: 50, | |
| }); | |
| bench | |
| .add('Math.random()', () => { | |
| globalThis.__sink = Math.random(); | |
| }) | |
| .add('Math.random() * 100 + 50', () => { | |
| globalThis.__sink = Math.random() * 100 + 50; | |
| }) | |
| .add('Math.floor(Math.random() * 6)', () => { | |
| globalThis.__sink = Math.floor(Math.random() * 6); | |
| }) | |
| .add('fisherYates(10)', () => { | |
| globalThis.__sink = fisherYates10()[0]; | |
| }) | |
| .add('crypto.getRandomValues(Uint32Array(1))', () => { | |
| getRandomValues(singleBuf); | |
| globalThis.__sink = singleBuf[0]; | |
| }) | |
| .add('batched getRandomValues (BATCH=1024)', () => { | |
| globalThis.__sink = nextBatchedUint32(); | |
| }) | |
| .add('crypto.randomUUID()', () => { | |
| globalThis.__sink = randomUUID(); | |
| }) | |
| .add('crypto.randomInt(0, 256)', () => { | |
| globalThis.__sink = randomInt(0, 256); | |
| }) | |
| .add('crypto.randomBytes(16)', () => { | |
| globalThis.__sink = randomBytes(16)[0]; | |
| }) | |
| .add('unbiasedInt via getRandomValues', () => { | |
| globalThis.__sink = unbiasedIntCrypto(6); | |
| }); | |
| console.log('='.repeat(96)); | |
| console.log('Environment'); | |
| console.log('-'.repeat(96)); | |
| console.log(` node ${process.version}`); | |
| console.log(` platform ${os.platform()} ${os.release()} (${os.arch()})`); | |
| const cpuModel = os.cpus()[0].model.trim() || 'unknown'; | |
| const cpuSpeed = os.cpus()[0].speed; | |
| console.log(` cpu ${cpuModel}${cpuSpeed > 0 ? ` @ ${(cpuSpeed / 1000).toFixed(2)} GHz` : ''}`); | |
| console.log(` cores ${os.cpus().length}`); | |
| console.log(` total memory ${(os.totalmem() / 1024 ** 3).toFixed(1)} GB`); | |
| console.log(` date ${new Date().toISOString()}`); | |
| console.log('='.repeat(96)); | |
| console.log(); | |
| await bench.run(); | |
| // tinybench reports latency in milliseconds. Convert to ns for readability. | |
| const MS_TO_NS = 1_000_000; | |
| const rows = bench.tasks.map((t) => ({ | |
| name: t.name, | |
| medianNs: t.result.latency.p50 * MS_TO_NS, | |
| p75Ns: t.result.latency.p75 * MS_TO_NS, | |
| p99Ns: t.result.latency.p99 * MS_TO_NS, | |
| rme: t.result.latency.rme, | |
| samples: t.result.latency.samplesCount, | |
| })); | |
| rows.sort((a, b) => a.medianNs - b.medianNs); | |
| const baseline = rows.find((r) => r.name === 'Math.random()').medianNs; | |
| const fmt = (ns) => { | |
| if (ns >= 1_000_000) return `${(ns / 1_000_000).toFixed(2)} ms`; | |
| if (ns >= 1_000) return `${(ns / 1_000).toFixed(2)} µs`; | |
| return `${ns.toFixed(1)} ns`; | |
| }; | |
| console.log('Results (sorted by median latency, lower is faster)'); | |
| console.log('-'.repeat(96)); | |
| console.log( | |
| 'task'.padEnd(44) + | |
| 'median'.padStart(12) + | |
| 'p75'.padStart(12) + | |
| 'p99'.padStart(12) + | |
| '±rme'.padStart(10) + | |
| 'samples'.padStart(14) | |
| ); | |
| console.log('-'.repeat(96)); | |
| for (const r of rows) { | |
| console.log( | |
| r.name.padEnd(44) + | |
| fmt(r.medianNs).padStart(12) + | |
| fmt(r.p75Ns).padStart(12) + | |
| fmt(r.p99Ns).padStart(12) + | |
| `±${r.rme.toFixed(1)}%`.padStart(10) + | |
| r.samples.toLocaleString().padStart(14) | |
| ); | |
| } | |
| console.log('-'.repeat(96)); | |
| console.log(); | |
| console.log('Relative cost vs Math.random() (by median):'); | |
| console.log('-'.repeat(96)); | |
| for (const r of rows) { | |
| const ratio = r.medianNs / baseline; | |
| console.log(` ${r.name.padEnd(42)} ${ratio.toFixed(1).padStart(7)}×`); | |
| } | |
| console.log('='.repeat(96)); | |
| if (globalThis.__sink === undefined) throw new Error('sink elided'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment