A quick description of a 32-bit state Weyl sequence (logically additive recurrence) followed by a bit-finializing operation which can be used for generating permutations of $\left[0,~2^{32}\right)$ or for random number generation which supports random access queries. The goal is to find a sweet-spot of time complexity, statistical quality and number of unique statistically independent permutations generated.

Bullet-points:

Random access is the key point. If that’s not needed then there are a number of other options.

Structurally equivalent to the 64-bit version described HERE.

Passes Smallcrush .

Smallcrush . Single and/or multiple stream (aka different permutations)

Example implementation as a public domain single header file library (lprns.h).

Approximately the same complexity as the roughly equivalent PCG generator: godbolt. We’re trading statistical quality for random access operations.



Briefly the state is updated by a Weyl sequence which allows very fast queries. To provide reasonable statistical quality this is then passed through a bit finalizing function. Example from the library:

// non-stream variant has no second parameter and multiplier is a constant static inline uint32_t lprns_stream_mix ( uint32_t x , uint32_t m ) { x ^= x >> LPRNS_S0 ; x *= m ; x ^= x >> LPRNS_S1 ; x *= LPRNS_M1 ; x ^= x >> LPRNS_S2 ; return x ; }



Non-paramaterized version loses the second input parameter and m becomes a constant. This is common-ish finalizer used (for example) in murmurhash 3, xxhash, etc. In an ideal world I’d have run a search/optimization to provide the constants. Constants from murmur/xxhash are designed for high-entropy input with a goodness measure of bit avalanche. Here we have a low entropy input (a low-discrepancy sequence) which we want to transform into a reasonable pseudo-random sequence. Since it’s doing good enough at testing…I couldn’t be bothered.

The example code above requires each stream to have a pretty good constant m otherwise statistical quality will be decreased. The library provides an inital value and function to create the next from it, etc. The function simply drops the bottom 3 bits (fixed at 101), computes the next larger integer with the same population count and sets the bottom 3 bits to 101. Minimal testing indicates that this scheme works well but that doesn’t mean much. Frankly I don’t consider it very interesting. Getting the m values is a pain, its probably one more extra thing to store and it limits the number of unique permutations. Tossing it out since these downsides might not be an issue for some cases.

Recently I discovered that my 64-bit variant is virtually identical to a generator in the Java system library informally called splitmix. This generator supports multiple streams by parameterizing the Weyl constant. Although somewhat easier than the version above all of the same problems apply here as well. Instead let’s toss together one that allows any unique integer to define a stream:

// alternate streaming variant static inline uint32_t lprns_stream_mix ( uint32_t x , uint32_t m ) { x ^= m ; // any integer 'm' gives a unique stream x ^= x >> LPRNS_S0 ; x *= LPRNS_M0 ; x ^= x >> LPRNS_S1 ; x *= LPRNS_M1 ; x ^= x >> LPRNS_S2 ; return x ; }



The previous is less than ideal in my eyes since we have two $\mathbb{F}_2$ operations in a row (xor with m followed by an xorshift) but I’m attempting to not extended the operation chain any longer than needed. If lower quality is fine we can drop one xorshift-multiply (and say just grab murmurhash 2 constants) or even lower by dropping all after then xor by m and returning a product with a good (M)LCG constant. The last becomes equivalent to a minimal 2D white noise generator I previously posted about.



