PRNGs Are Not That Random

Pseudorandom number generators (PRNG) are ever so prevalent, yet highly disputed. If you honestly think about it, how can you ever get a computer to calculate pure randomness? You can’t.

Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin.

- John von Neumann

Terminology

The reason we cannot generate randomness is because a PRNG is dependent on an initial seed. A seed is a just an initial value in an algorithm to generate a given number. This could be a level of noise, the time (in milliseconds), really anything. For instance, to generate a random number in C, we pass the number of seconds since the UNIX epoch seed into srand():

srand((unsigned) time(&t)); double random = rand();

A seed is formally known as a state, and each digit in the state is denoted by a bit.

Since we can never reach pure randomness — after generating n numbers, we will see our initial value again. The exact number it takes to find the repeating value is defined by the periodicity (or cycle in combinatorics). A 2-digit seed will have a period length of at most 100, 3-digits at most 1,000, and 4-digits at most 10,000. The set of all the unique numbers generated by a n digit number and it’s cardinality the upper-bound limit. It’s quite obvious if you relate this to brute-force password cracking. Take a 5 digit password, there are 10 numbers to guess for each digit (0–9), coming out to $10^5$.

Middle-Squares Method

The Middle-Squares method earliest pseudo-random number algorithm was discovered by John von Neumann. It is as follows:

Take an initial seed of length m Square it Take the middle m digits as your new value Square it Repeat steps 2–4 for x number of times

I wrote a simple Middle-Squared algorithm here:

public static void main(String[] args) { HashSet<Integer> randomNumbers = new HashSet<>(); Random rand = new Random(); int numberGenerated = rand.nextInt(); int iterations = 0; while(!randomNumbers.contains(numberGenerated)) { randomNumbers.add(numberGenerated); int newIteration = getNewIteration(numberGenerated); numberGenerated = (int) Math.pow(newIteration, 2); iterations++; } } public static int getNewIteration(int numberGenerated) { String numberGeneratedString = String.valueOf(numberGenerated); int length = numberGeneratedString.length(); String newIteration = numberGeneratedString.substring(length/2 — length/4, length/2 + length/4); System.out.println(“Old number is “ + numberGenerated + “. New number is “ + newIteration); return Integer.parseInt(newIteration); }

Here is the output:

Old number is 169560864. New number is 9560 Old number is 91393600. New number is 3936 Old number is 15492096. New number is 4920 Old number is 24206400. New number is 2064 Old number is 4260096. New number is 60 Old number is 3600. New number is 60 Number of iterations: 6

One of the biggest flaws in this design is how quickly this converges to 0. However, since then there have been many discoveries in the PRNG field.

Linear Congruential Generators The most prevalent PRNG type is the Linear Congruential Generator (LCG). It is based on the following recurrence relation:

$$X_{n+1} = (aX_n+c) \pmod m$$

The modulus $m$, $m > 0$, multiplier $a$, $m > a >0$, incremented by $c$, $m > c \geq 0$. Like the Middle-Squared Method, it relies on a seed value, denoted as $X_0$, $m > X_0 \geq 0$.

The algorithm is described in more detail here.

Just to give some perspective, here is the source code for Java’s pseudo-random number generator:

public Random(long seed) { setSeed(seed); } public synchronized void setSeed(long seed) { this.seed = (seed ^ 0x5DEECE66DL) & ((1L << 48) — 1); haveNextNextGaussian = false; } // (multiplier) 0x5DEECE66DL = 25214903917 in decimal // (incrementer) 0xBL = 11 in decimal //Right shifts the seed by the “bits” argument protected synchronized int next(int bits) { seed = (seed * 0x5DEECE66DL + 0xBL) & ((1L << 48) — 1); return (int) (seed >>> (48 — bits)); }

As you can see, Java uses $2^{48}$, 25214903917, and 11 for the modulus, multiplier, and increment, respectively. The number of bits used in the output is determined by the argument to next(). Therefore, nextInt() uses 32 bits. However, because $2^{48}$ would produce an IntegerOverflow, Java strips the $n^{th}$ most significant bits through right shifting. This is due to a higher period in these bits in comparison to the others.

LCG Advantages and Disadvantages

The main advantages an LCG has to offer are speed and memory. Of all the drawbacks, the most concerning is if the modulus is a power of 2, then least significant bits have a drastically smaller period. Namely, if $m = 2^a$, then its period is $2^a$. This is displayed within nextInt():

public int nextInt(int n) { if (n <= 0) throw new IllegalArgumentException(“n must be positive”); if ((n & -n) == n) return (int) ((n * (long) next(31)) >> 31); int bits, val; do { bits = next(31); val = bits % n; } while (bits — val + (n — 1) < 0); return val; }

Conclusion

I hope this gives a glimpse into the history and implementation of Pseudorandom Number Generators. There is a copious amount of PRNGs, however, I wanted to touch on the two most widely-used ones. It is easy to see where there can be drawbacks using one PRNG versus another — and if you plan on implementing one, to take these into consideration.

For those that play video games, you can relate!

Most of my research on PRNGs came from Donald Knuth’s The Art of Computer Programming Volume II.

As always, feel free to check out my Github to check out what I’m currently working on!