Skip to content

Instantly share code, notes, and snippets.

@blixt
Last active January 14, 2024 07:01
Show Gist options
  • Save blixt/f17b47c62508be59987b to your computer and use it in GitHub Desktop.
Save blixt/f17b47c62508be59987b to your computer and use it in GitHub Desktop.
A very simple, seedable JavaScript PRNG. NOTE: Please read comments on why this is not a good choice.
// NOTICE 2020-04-18
// Please see the comments below about why this is not a great PRNG.
// Read summary by @bryc here:
// https://github.com/bryc/code/blob/master/jshash/PRNGs.md
// Have a look at js-arbit which uses Alea:
// https://github.com/blixt/js-arbit
/**
* Creates a pseudo-random value generator. The seed must be an integer.
*
* Uses an optimized version of the Park-Miller PRNG.
* http://www.firstpr.com.au/dsp/rand31/
*/
function Random(seed) {
this._seed = seed % 2147483647;
if (this._seed <= 0) this._seed += 2147483646;
}
/**
* Returns a pseudo-random value between 1 and 2^32 - 2.
*/
Random.prototype.next = function () {
return this._seed = this._seed * 16807 % 2147483647;
};
/**
* Returns a pseudo-random floating point number in range [0, 1).
*/
Random.prototype.nextFloat = function (opt_minOrMax, opt_max) {
// We know that result of next() will be 1 to 2147483646 (inclusive).
return (this.next() - 1) / 2147483646;
};
@nanodeath
Copy link

@blixt this page happens to be the #2 result on Google for "javascript prng" 🙂

@dgreensp
Copy link

dgreensp commented Mar 12, 2021

The comment by @bryc above about the problem with this PRNG always returning integers ending in 0x80 is wrong, because it uses an incorrectly "optimized" version of the PRNG.

// this is the correct PRNG (EDIT: actually no, see my comment below, the Math.imul already breaks it)
function LCG(a) {
    return function() {
      a = Math.imul(48271, a) | 0 % 2147483647;
      return (a & 2147483647) / 2147483648;
    }
}

// This is wrong.  Masking with 2**31-1 is not the same as modulo that number,
// just as x&255 is not the same as x%255, it is x%256.
var LCG=s=>()=>(2**31-1&(s=Math.imul(48271,s)))/2**31; // Same as above

So the OP is not as bad as suggested.

@bryc
Copy link

bryc commented Mar 12, 2021

Edit: I must apologize as I was displaying a confirmation bias. But @dgreensp is correct; the 0x80 problem I mentioned only occurs because the multiplication result is smaller than the modulus. And he is right that the randomness quality is worse than a vanilla LCG. But even the original is quite poor quality. I would only advise using an LCG if you want to study the math properly, learn how to test randomness properly, and ultimately do not mind using a very basic PRNG that won't fool randomness tests like PractRand and TestU01.

For reference, here is a clear (and correct) implementation:

function LCG(a) {
    return function() {
      a = (a * 48271) % 2147483647;
      return (a - 1) / 2147483646;
    }
}

Original text below:


@dgreensp

Sorry but that is nonsense. Using Math.imul forces it to be a 32-bit LCG, which makes the AND and Modulo operators do the exact same thing because the operand is never over 2147483647. I simply chose AND because it produces more simplified byte code that Firefox can JIT better. The original C code requires 64-bit long type for the multiplication stage. My JS implementation essentially changes long to int, and yes there are implications for doing that.

function LCG(a) {
    return function() {
      return a = a * 48271 % 2147483647;
    }
}

The above code is a C-compatible version, but it's slower. The only reason it's even possible is because the result of the multiplication is never more than 48271 * 2147483647. So you can't use larger LCG multipliers. The JavaScript JIT compilers cannot as easily accelerate these straight Number multiplications as they can for Math.imul or other 32-bit casted operations. And in all honesty, I highly doubt it makes a difference quality-wise. These old LCGs fail modern PRNG tests, it's as simple as that. There are better, faster options available.

The point of my various LCG examples aren't that it 'produces the same exact output as the 1988 paper', but is a version more optimized for use in JS. The 0x80 problem I mentioned is not in error; it will affect any 32-bit LCG, even if implemented in C/C++. It's something to be aware of when writing optimized LCGs in JS.

Edit: Here is a performance case test with some sample comparison code: https://jsbench.me/1fkm5vl0rc/1

Firefox: A is fastest, B is ~55% slower, C is ~65% slower
Chrome: A and B are mostly on par (implying smarter modulo optimization). C is ~55% slower

So that may explain why I'd use AND over Modulo.

But again; LCGs do not pass randomness tests and require 64-bit or even 128-bit multiplication for best results. PCG is basically a modern adaptation of LCG which does pass randomness tests, but it is ill-suited for JavaScript because it relies on overflowing 64-bit multiplication.

@dgreensp
Copy link

@bryc Sorry, I didn't mean to use the Math.imul version of the code in my "correct" example, that was an error on my part.

The main point I was trying to make is, using 2^31-1 (= 2147483647) as a modulus is clever because 2147483647 is prime, and in particular, you will not have problems with there being a pattern to the low bits. When you switch to using & or Math.imul, it's not the same algorithm, and it's known to have way worse properties because the modulus is a power of two. There is at least one place above in this thread where the switch is quietly made from a prime modulus to a power-of-two modulus, keeping the numbers 16807 and 2^31-1 in there, and it's hard to spot, because there's a comment stating it's the same algorithm.

Anyway, for my purposes (generating some predictable randomness simply and efficiently for randomized unit tests), I actually like the original LCG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment