Fast 8-bit pseudorandom number generator
While doing some retro-programming, I was after a small, fast random number generator for an old computer (specifically, a Tandy TRS-80 Colour Computer I, with 895kHz Motorola 6809 CPU). Modern algorithms tend to keep large state, or do operations that would be "hard" on an old 8-bit microcomputer (eg. multiply, divide, wide operations). After trawling the internet for too long, I came across Ultra Fast Pseudorandom number generator for 8-bit (PRNG), by EternityForest, which appeared perfect for my needs. It maintains 4 bytes of state, and so in theory could have a cycle of around [math]\displaystyle{ 2^{32} }[/math] (see note below, it doesn't), which is far better than I need, and most importantly, it's a small number of simple, fast 8-bit ops - no multiply ops, no modulo ops, no divides!
Considering the size of the state (32 bits), we can brute-force map all the cycles (only takes a couple of minutes, single threaded). Doing this, we see that there are two cycles of a little over one billion in length. However, there are also 4 cycles of only 256 length. Starting seed value is important, and, interestingly, (2,0,0,0) results in one of the long cycles. This is more than adequate for simple games and such on an 8-bit micro. I don't know where the discrepancy with the original article comes from, although I did not use grep in my testing, but rather wrote code to map out all possible state values:
Length | Count | Seeds (a,b,c,x) |
---|---|---|
1080738560 | 2 | 02,00,00,00 03,00,00,00 |
487780608 | 2 | 00,00,00,00 01,00,00,00 |
267577088 | 2 | 17,00,00,00 27,00,00,00 |
58978560 | 2 | 0A,00,00,00 21,00,00,00 |
56331776 | 2 | 0E,00,00,00 1F,00,00,00 |
51243520 | 2 | 19,00,00,00 37,00,00,00 |
47012352 | 2 | 12,00,00,00 2F,00,00,00 |
39644928 | 2 | D7,00,00,00 EA,00,00,00 |
26927360 | 2 | 1C,00,00,00 34,00,00,00 |
15374336 | 2 | 86,00,00,00 AE,00,00,00 |
12645632 | 1 | 25,01,00,00 |
6263552 | 1 | 2B,08,00,00 |
5651712 | 1 | D4,00,00,00 |
2513408 | 2 | ED,00,00,00 B1,01,00,00 |
509440 | 2 | 70,0C,00,00 03,52,00,00 |
326400 | 2 | 60,0F,00,00 E9,28,00,00 |
302336 | 1 | 41,0F,00,00 |
54016 | 1 | 1A,A1,01,00 |
28928 | 1 | B5,46,01,00 |
27904 | 1 | 1A,DB,03,00 |
19456 | 2 | 50,09,02,00 03,C7,03,00 |
18176 | 1 | 6E,85,01,00 |
8704 | 2 | E2,4C,05,00 AA,61,14,00 |
512 | 2 | D0,98,19,00 79,28,BA,00 |
256 | 4 | 00,02,01,00 00,03,01,00 74,5E,21,00 54,B8,3C,00 |
4294967296 | 44 |
Noting that it is somewhat unusual to see a plain bit shift here rather than a rotate, we can explore how changing to a rotate changes the cycles. The changes are significant, and, notably, (0,0,0,0) is now a 2.8B long cycle:
Length | Count | Seeds (a,b,c,x) |
---|---|---|
2826386176 | 1 | 00,00,00,00 |
653676288 | 1 | 10,00,00,00 |
630179072 | 1 | 11,00,00,00 |
136099072 | 1 | 2A,00,00,00 |
19772672 | 2 | 7B,00,00,00 41,02,00,00 |
3474944 | 2 | 5E,02,00,00 BC,04,00,00 |
647936 | 1 | E2,34,00,00 |
401152 | 1 | 52,27,00,00 |
224512 | 1 | 3E,19,00,00 |
196864 | 1 | 2B,38,00,00 |
156160 | 2 | 70,27,00,00 03,83,01,00 |
90112 | 2 | FE,3A,00,00 D1,CE,00,00 |
66048 | 2 | 1A,0B,01,00 EA,8D,01,00 |
9472 | 2 | 65,1D,06,00 53,7E,15,00 |
5376 | 2 | E8,19,01,00 5D,9D,0C,00 |
2048 | 2 | F0,D3,0D,00 7A,F9,54,00 |
512 | 4 | 4C,95,3A,00 CF,EC,55,00 97,F6,70,00 E6,26,D3,00 |
256 | 2 | 00,02,01,00 7A,9D,21,00 |
4294967296 | 30 |
C Implementation
The C-code from the link, formatted and most comments stripped, and modified to use a rotate, is:
/***
X ABC Algorithm Random Number Generator for 8-Bit Devices
https://www.electro-tech-online.com/threads/ultra-fast-pseudorandom-number-generator-for-8-bit.124249/
Not safe for cryptographic use!
2025-08-04: Modified to use rotate
***/
static uint8_t a, b, c, x;
/* return 8-bit pseudorandom number */
uint8_t rnd8() {
x++;
a = (a ^ c) ^ x;
b = b + a;
c = (c + ((b >> 1) | (b << 7))) ^ a;
return c;
}
/* Add entropy into the state */
void init_rng(uint8_t s1, uint8_t s2, uint8_t s3) {
/* XOR new entropy into key state */
a ^= s1;
b ^= s2;
c ^= s3;
rnd8();
}
6809 Assembler
Code in Tandy CoCo EDTASM format, 52 or 54 cycles, not including the BSR. Using Direct Page (DP) addressing for the 4 bytes of state shaves 10 cycles. I've been doing this in various code where I value performance, using page 0 bytes $00
through $03
.
00010 * 8-BIT RANDOM NUMBER
00020 * GENERATOR
00030 * RANDOM RETURNED IN A
00040 * TOTAL: 52 OR 54 CYCLES
00050 * ANNOTATED WITH CYCLES
00060 * USES DIRECT ADDRESSING
00070 * (DP REG) FOR SPEED
00080 SETDP $0
00090 RNDA EQU $0
00100 RNDB EQU $1
00110 RNDC EQU $2
00120 RNDX EQU $3
00130 RND INC <RNDX 6
00140 LDA <RNDA 4
00150 EORA <RNDC 4
00160 EORA <RNDX 4
00170 STA <RNDA 4
00180 ADDA <RNDB 4
00190 STA <RNDB 4
00200 LSRA 2
00210 BCC RND1 3
00220 ORA #$80 2
00230 RND1 ADDA <RNDC 4
00240 EORA <RNDA 4
00250 STA <RNDC 4
00260 RTS 5
DieHarder results
These are the results not using a rotate, and seeded with 2,0,0,0 to get one of the long cycles. Yes, not perfect, but quite good considering the simplicity.
$ ./rnd8 -g | dieharder -g 200 -a
#=============================================================================#
# dieharder version 3.31.1 Copyright 2003 Robert G. Brown #
#=============================================================================#
rng_name |rands/second| Seed |
stdin_input_raw| 9.21e+07 |1273947027|
#=============================================================================#
test_name |ntup| tsamples |psamples| p-value |Assessment
#=============================================================================#
diehard_birthdays| 0| 100| 100|0.88292205| PASSED
diehard_operm5| 0| 1000000| 100|0.00000000| FAILED
diehard_rank_32x32| 0| 40000| 100|0.19738318| PASSED
diehard_rank_6x8| 0| 100000| 100|0.35564240| PASSED
diehard_bitstream| 0| 2097152| 100|0.27995945| PASSED
diehard_opso| 0| 2097152| 100|0.00000000| FAILED
diehard_oqso| 0| 2097152| 100|0.00000000| FAILED
diehard_dna| 0| 2097152| 100|0.00000000| FAILED
diehard_count_1s_str| 0| 256000| 100|0.00000000| FAILED
diehard_count_1s_byt| 0| 256000| 100|0.00000000| FAILED
diehard_parking_lot| 0| 12000| 100|0.00000148| WEAK
diehard_2dsphere| 2| 8000| 100|0.00000068| FAILED
diehard_3dsphere| 3| 4000| 100|0.23194755| PASSED
diehard_squeeze| 0| 100000| 100|0.00000000| FAILED
diehard_sums| 0| 100| 100|0.51356957| PASSED
diehard_runs| 0| 100000| 100|0.00000000| FAILED
diehard_runs| 0| 100000| 100|0.00000000| FAILED
diehard_craps| 0| 200000| 100|0.00000000| FAILED
diehard_craps| 0| 200000| 100|0.00000000| FAILED
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000000| FAILED
marsaglia_tsang_gcd| 0| 10000000| 100|0.00000278| WEAK
sts_monobit| 1| 100000| 100|0.09547519| PASSED
sts_runs| 2| 100000| 100|0.57044343| PASSED
sts_serial| 1| 100000| 100|0.08768227| PASSED
sts_serial| 2| 100000| 100|0.04726299| PASSED
sts_serial| 3| 100000| 100|0.16386173| PASSED
sts_serial| 3| 100000| 100|0.88479768| PASSED
sts_serial| 4| 100000| 100|0.80617784| PASSED
sts_serial| 4| 100000| 100|0.20542621| PASSED
sts_serial| 5| 100000| 100|0.81735426| PASSED
sts_serial| 5| 100000| 100|0.44647962| PASSED
sts_serial| 6| 100000| 100|0.36605077| PASSED
sts_serial| 6| 100000| 100|0.34798929| PASSED
sts_serial| 7| 100000| 100|0.43410266| PASSED
sts_serial| 7| 100000| 100|0.64300880| PASSED
sts_serial| 8| 100000| 100|0.15769301| PASSED
sts_serial| 8| 100000| 100|0.16589708| PASSED
sts_serial| 9| 100000| 100|0.35759181| PASSED
sts_serial| 9| 100000| 100|0.16977557| PASSED
sts_serial| 10| 100000| 100|0.09585210| PASSED
sts_serial| 10| 100000| 100|0.07508614| PASSED
sts_serial| 11| 100000| 100|0.19803759| PASSED
sts_serial| 11| 100000| 100|0.58540028| PASSED
sts_serial| 12| 100000| 100|0.19934867| PASSED
sts_serial| 12| 100000| 100|0.85743500| PASSED
sts_serial| 13| 100000| 100|0.59817269| PASSED
sts_serial| 13| 100000| 100|0.32571486| PASSED
sts_serial| 14| 100000| 100|0.98000563| PASSED
sts_serial| 14| 100000| 100|0.17949475| PASSED
sts_serial| 15| 100000| 100|0.99923800| WEAK
sts_serial| 15| 100000| 100|0.43646508| PASSED
sts_serial| 16| 100000| 100|0.99474339| PASSED
sts_serial| 16| 100000| 100|0.82177414| PASSED
rgb_bitdist| 1| 100000| 100|0.00000000| FAILED
rgb_bitdist| 2| 100000| 100|0.00000000| FAILED
rgb_bitdist| 3| 100000| 100|0.55397463| PASSED
rgb_bitdist| 4| 100000| 100|0.00467593| WEAK
rgb_bitdist| 5| 100000| 100|0.25400332| PASSED
rgb_bitdist| 6| 100000| 100|0.06882791| PASSED
rgb_bitdist| 7| 100000| 100|0.99999840| WEAK
rgb_bitdist| 8| 100000| 100|0.76230931| PASSED
rgb_bitdist| 9| 100000| 100|0.05068579| PASSED
rgb_bitdist| 10| 100000| 100|0.52705506| PASSED
rgb_bitdist| 11| 100000| 100|0.08959456| PASSED
rgb_bitdist| 12| 100000| 100|0.22353506| PASSED
rgb_minimum_distance| 2| 10000| 1000|0.00000000| FAILED
rgb_minimum_distance| 3| 10000| 1000|0.00100107| WEAK
rgb_minimum_distance| 4| 10000| 1000|0.00001416| WEAK
rgb_minimum_distance| 5| 10000| 1000|0.00002256| WEAK
rgb_permutations| 2| 100000| 100|0.00000000| FAILED
rgb_permutations| 3| 100000| 100|0.00000000| FAILED
rgb_permutations| 4| 100000| 100|0.00000000| FAILED
rgb_permutations| 5| 100000| 100|0.00000000| FAILED
rgb_lagged_sum| 0| 1000000| 100|0.95861990| PASSED
rgb_lagged_sum| 1| 1000000| 100|0.72880186| PASSED
rgb_lagged_sum| 2| 1000000| 100|0.29448918| PASSED
rgb_lagged_sum| 3| 1000000| 100|0.70397671| PASSED
rgb_lagged_sum| 4| 1000000| 100|0.99820420| WEAK
rgb_lagged_sum| 5| 1000000| 100|0.01396900| PASSED
rgb_lagged_sum| 6| 1000000| 100|0.36742497| PASSED
rgb_lagged_sum| 7| 1000000| 100|0.63245300| PASSED
rgb_lagged_sum| 8| 1000000| 100|0.24020279| PASSED
rgb_lagged_sum| 9| 1000000| 100|0.31982465| PASSED
rgb_lagged_sum| 10| 1000000| 100|0.35670987| PASSED
rgb_lagged_sum| 11| 1000000| 100|0.00003249| WEAK
rgb_lagged_sum| 12| 1000000| 100|0.53488961| PASSED
rgb_lagged_sum| 13| 1000000| 100|0.00041943| WEAK
rgb_lagged_sum| 14| 1000000| 100|0.57104632| PASSED
rgb_lagged_sum| 15| 1000000| 100|0.00063319| WEAK
rgb_lagged_sum| 16| 1000000| 100|0.59203723| PASSED
rgb_lagged_sum| 17| 1000000| 100|0.01943114| PASSED
rgb_lagged_sum| 18| 1000000| 100|0.91065056| PASSED
rgb_lagged_sum| 19| 1000000| 100|0.35803767| PASSED
rgb_lagged_sum| 20| 1000000| 100|0.07746578| PASSED
rgb_lagged_sum| 21| 1000000| 100|0.08947439| PASSED
rgb_lagged_sum| 22| 1000000| 100|0.24041574| PASSED
rgb_lagged_sum| 23| 1000000| 100|0.00000000| FAILED
rgb_lagged_sum| 24| 1000000| 100|0.49325189| PASSED
rgb_lagged_sum| 25| 1000000| 100|0.22372856| PASSED
rgb_lagged_sum| 26| 1000000| 100|0.29554946| PASSED
rgb_lagged_sum| 27| 1000000| 100|0.00072153| WEAK
rgb_lagged_sum| 28| 1000000| 100|0.95387180| PASSED
rgb_lagged_sum| 29| 1000000| 100|0.00075700| WEAK
rgb_lagged_sum| 30| 1000000| 100|0.80628988| PASSED
rgb_lagged_sum| 31| 1000000| 100|0.00203434| WEAK
rgb_lagged_sum| 32| 1000000| 100|0.22492701| PASSED
rgb_kstest_test| 0| 10000| 1000|0.00736559| PASSED
dab_bytedistrib| 0| 51200000| 1|0.00000000| FAILED
dab_dct| 256| 50000| 1|0.00000000| FAILED
Preparing to run test 207. ntuple = 0
dab_filltree| 32| 15000000| 1|0.00000000| FAILED
dab_filltree| 32| 15000000| 1|0.00000000| FAILED
Preparing to run test 208. ntuple = 0
dab_filltree2| 0| 5000000| 1|0.33648502| PASSED
dab_filltree2| 1| 5000000| 1|0.00000232| WEAK
Preparing to run test 209. ntuple = 0
dab_monobit2| 12| 65000000| 1|1.00000000| FAILED
Changing to a rotate improves the DieHarder results significantly:
$ ./rnd8 -g | dieharder -g 200 -a
#=============================================================================#
# dieharder version 3.31.1 Copyright 2003 Robert G. Brown #
#=============================================================================#
rng_name |rands/second| Seed |
stdin_input_raw| 9.44e+07 |2789373216|
#=============================================================================#
test_name |ntup| tsamples |psamples| p-value |Assessment
#=============================================================================#
diehard_birthdays| 0| 100| 100|0.73136101| PASSED
diehard_operm5| 0| 1000000| 100|0.90927682| PASSED
diehard_rank_32x32| 0| 40000| 100|0.27039526| PASSED
diehard_rank_6x8| 0| 100000| 100|0.38487152| PASSED
diehard_bitstream| 0| 2097152| 100|0.96317285| PASSED
diehard_opso| 0| 2097152| 100|0.00000000| FAILED
diehard_oqso| 0| 2097152| 100|0.00000008| FAILED
diehard_dna| 0| 2097152| 100|0.64627244| PASSED
diehard_count_1s_str| 0| 256000| 100|0.75700124| PASSED
diehard_count_1s_byt| 0| 256000| 100|0.97101586| PASSED
diehard_parking_lot| 0| 12000| 100|0.92327672| PASSED
diehard_2dsphere| 2| 8000| 100|0.21233077| PASSED
diehard_3dsphere| 3| 4000| 100|0.84345990| PASSED
diehard_squeeze| 0| 100000| 100|0.63161458| PASSED
diehard_sums| 0| 100| 100|0.07300803| PASSED
diehard_runs| 0| 100000| 100|0.42578001| PASSED
diehard_runs| 0| 100000| 100|0.19783564| PASSED
diehard_craps| 0| 200000| 100|0.39721171| PASSED
diehard_craps| 0| 200000| 100|0.87909134| PASSED
marsaglia_tsang_gcd| 0| 10000000| 100|0.18626488| PASSED
marsaglia_tsang_gcd| 0| 10000000| 100|0.03672773| PASSED
sts_monobit| 1| 100000| 100|0.78987431| PASSED
sts_runs| 2| 100000| 100|0.75871498| PASSED
sts_serial| 1| 100000| 100|0.65907983| PASSED
sts_serial| 2| 100000| 100|0.80583429| PASSED
sts_serial| 3| 100000| 100|0.98522488| PASSED
sts_serial| 3| 100000| 100|0.97315612| PASSED
sts_serial| 4| 100000| 100|0.74517395| PASSED
sts_serial| 4| 100000| 100|0.99564586| WEAK
sts_serial| 5| 100000| 100|0.45415150| PASSED
sts_serial| 5| 100000| 100|0.44294273| PASSED
sts_serial| 6| 100000| 100|0.76267441| PASSED
sts_serial| 6| 100000| 100|0.87221426| PASSED
sts_serial| 7| 100000| 100|0.36513330| PASSED
sts_serial| 7| 100000| 100|0.15655257| PASSED
sts_serial| 8| 100000| 100|0.91098479| PASSED
sts_serial| 8| 100000| 100|0.74669717| PASSED
sts_serial| 9| 100000| 100|0.99448687| PASSED
sts_serial| 9| 100000| 100|0.89939664| PASSED
sts_serial| 10| 100000| 100|0.50762601| PASSED
sts_serial| 10| 100000| 100|0.63687484| PASSED
sts_serial| 11| 100000| 100|0.20453055| PASSED
sts_serial| 11| 100000| 100|0.84841538| PASSED
sts_serial| 12| 100000| 100|0.64412681| PASSED
sts_serial| 12| 100000| 100|0.71593572| PASSED
sts_serial| 13| 100000| 100|0.15197581| PASSED
sts_serial| 13| 100000| 100|0.28799024| PASSED
sts_serial| 14| 100000| 100|0.42723653| PASSED
sts_serial| 14| 100000| 100|0.87767186| PASSED
sts_serial| 15| 100000| 100|0.26301469| PASSED
sts_serial| 15| 100000| 100|0.40136257| PASSED
sts_serial| 16| 100000| 100|0.11677145| PASSED
sts_serial| 16| 100000| 100|0.05834049| PASSED
rgb_bitdist| 1| 100000| 100|0.04687653| PASSED
rgb_bitdist| 2| 100000| 100|0.40510824| PASSED
rgb_bitdist| 3| 100000| 100|0.44957201| PASSED
rgb_bitdist| 4| 100000| 100|0.99890820| WEAK
rgb_bitdist| 5| 100000| 100|0.56529265| PASSED
rgb_bitdist| 6| 100000| 100|0.21487515| PASSED
rgb_bitdist| 7| 100000| 100|0.84317157| PASSED
rgb_bitdist| 8| 100000| 100|0.98296530| PASSED
rgb_bitdist| 9| 100000| 100|0.47961161| PASSED
rgb_bitdist| 10| 100000| 100|0.22404213| PASSED
rgb_bitdist| 11| 100000| 100|0.54427027| PASSED
rgb_bitdist| 12| 100000| 100|0.37096246| PASSED
rgb_minimum_distance| 2| 10000| 1000|0.00429146| WEAK
rgb_minimum_distance| 3| 10000| 1000|0.74119383| PASSED
rgb_minimum_distance| 4| 10000| 1000|0.64599301| PASSED
rgb_minimum_distance| 5| 10000| 1000|0.30006254| PASSED
rgb_permutations| 2| 100000| 100|0.71961655| PASSED
rgb_permutations| 3| 100000| 100|0.76696985| PASSED
rgb_permutations| 4| 100000| 100|0.96088398| PASSED
rgb_permutations| 5| 100000| 100|0.89289481| PASSED
rgb_lagged_sum| 0| 1000000| 100|0.32294971| PASSED
rgb_lagged_sum| 1| 1000000| 100|0.96700645| PASSED
rgb_lagged_sum| 2| 1000000| 100|0.92747779| PASSED
rgb_lagged_sum| 3| 1000000| 100|0.27590456| PASSED
rgb_lagged_sum| 4| 1000000| 100|0.27713305| PASSED
rgb_lagged_sum| 5| 1000000| 100|0.07775178| PASSED
rgb_lagged_sum| 6| 1000000| 100|0.31477405| PASSED
rgb_lagged_sum| 7| 1000000| 100|0.98501873| PASSED
rgb_lagged_sum| 8| 1000000| 100|0.93098155| PASSED
rgb_lagged_sum| 9| 1000000| 100|0.48433679| PASSED
rgb_lagged_sum| 10| 1000000| 100|0.68513100| PASSED
rgb_lagged_sum| 11| 1000000| 100|0.80526210| PASSED
rgb_lagged_sum| 12| 1000000| 100|0.52042701| PASSED
rgb_lagged_sum| 13| 1000000| 100|0.61478030| PASSED
rgb_lagged_sum| 14| 1000000| 100|0.33356875| PASSED
rgb_lagged_sum| 15| 1000000| 100|0.60639394| PASSED
rgb_lagged_sum| 16| 1000000| 100|0.93078836| PASSED
rgb_lagged_sum| 17| 1000000| 100|0.26637020| PASSED
rgb_lagged_sum| 18| 1000000| 100|0.05588453| PASSED
rgb_lagged_sum| 19| 1000000| 100|0.87494882| PASSED
rgb_lagged_sum| 20| 1000000| 100|0.60864482| PASSED
rgb_lagged_sum| 21| 1000000| 100|0.30301534| PASSED
rgb_lagged_sum| 22| 1000000| 100|0.84049173| PASSED
rgb_lagged_sum| 23| 1000000| 100|0.37071523| PASSED
rgb_lagged_sum| 24| 1000000| 100|0.15466590| PASSED
rgb_lagged_sum| 25| 1000000| 100|0.41503049| PASSED
rgb_lagged_sum| 26| 1000000| 100|0.41965802| PASSED
rgb_lagged_sum| 27| 1000000| 100|0.65811802| PASSED
rgb_lagged_sum| 28| 1000000| 100|0.91346595| PASSED
rgb_lagged_sum| 29| 1000000| 100|0.80374128| PASSED
rgb_lagged_sum| 30| 1000000| 100|0.16208721| PASSED
rgb_lagged_sum| 31| 1000000| 100|0.21255093| PASSED
rgb_lagged_sum| 32| 1000000| 100|0.45037027| PASSED
rgb_kstest_test| 0| 10000| 1000|0.86836759| PASSED
dab_bytedistrib| 0| 51200000| 1|0.96762188| PASSED
dab_dct| 256| 50000| 1|0.89901805| PASSED
Preparing to run test 207. ntuple = 0
dab_filltree| 32| 15000000| 1|0.17450183| PASSED
dab_filltree| 32| 15000000| 1|0.88428199| PASSED
Preparing to run test 208. ntuple = 0
dab_filltree2| 0| 5000000| 1|0.56179444| PASSED
dab_filltree2| 1| 5000000| 1|0.48069361| PASSED
Preparing to run test 209. ntuple = 0
dab_monobit2| 12| 65000000| 1|1.00000000| FAILED
Updates
- 2025-08-04: Updated page after finding a typo in my C++ test code (c = (c + (b >> 1)) ^ a; somehow became c = (c + (a >> 1)) ^ a;). This greatly improved the DieHarder results, and changed the cycle layout somewhat. After analysing the switch from a shift to a rotate and seeing the improvements, the code has been updated to use rotate.