Intro
I'm implementing a variant called PCG [1] of a Linear-Congruential Generator (LCG), which is an extremely simple pseudo-random number generator (PRNG) governed by the equation
$$S_{n+1} = aS_{n} + c \pmod{m}$$
where $S_i$ is the state $i$ calls of next()
after 0, and $a, c$ and $m$ are parameters that together determine the quality of the LCG. Full-period (order-$m$) LCGs occur iff:
- $\gcd(m,c)=1$
- $a-1$ divisible by all prime factors of $m$
- $a-1$ divisible by 4 if $m$ divisible by 4
See [2] for more details about LCGs.
Distance
The author of PCG claims that PCG offers the useful features of "jump-ahead" and "distance" [3]. I suspect that jump-ahead refers to the ability to skip "efficiently" $n$ calls to next()
(where "efficiently" usually means $O(\log n)$ or $O(\log m)$ time); This is borne out by the fact that PCG, LCG and ChaCha20 (counter mode) are listed as having that property, but the RC4-based Arc4Random doesn't and therefore isn't listed as having that property. Skip-ahead in LCGs can be easily done by exploiting recursively the equation
$$S_{n+2} = a(aS_{n} + c) + c = \underbrace{a^2}_\textrm{new $a$} S_{n} + \underbrace{(a+1)c}_\textrm{new $c$} \pmod{m}$$
in a binary-exponentiation-type loop. That leaves, however, the property of "distance". I strongly suspect that this refers to the ability to compute "efficiently", given the current states $S_i$ and $S_j$ of two generators, the distance $j-i$, which is the number of calls to next()
that bring a state $S_i$ to a state $S_j$ (again, "efficiently" usually means here $O(\log (j-i))$ or $O(\log m)$ time). The author claims PCG, LCG and counter-mode ciphers like ChaCha20 have it, but Xorshift and the rest do not.
My question lies here.
How do we compute the distance between two LCG states?
For counter-mode cipher PRNGs, the distance between two states is a simple subtraction of the numerical value of the states, since advancing a state is just an increment by 1 of the state variable. All the complexity of the PRNG is in the output function.
But how is it done for LCGs? As far as I understand, computing the difference between two states can be reduced to the problem of computing the distance $d(S_j, S_i)$ between the two states and the zero-state $S_0 = 0$:
$$d(S_j, S_i) = d(S_j, 0) - d(S_i, 0) = j-i$$
We know that the closed-form solution for skip-ahead by $k$ steps in LCGs is
$$S_{n+k} = a^k S_n + \frac{a^{k-1}}{a-1}c \pmod{m}$$
, and when $S_0 = 0$ this simplifies to
$$S_{k} = \frac{a^{k-1}}{a-1}c \pmod{m}$$
Suppose that we now have some state $S_n$ of unknown $n$ and we wish to determine $n$. After some algebra, we get
$$ \begin{align*} \frac{a-1}{c}S_{n} &= a^{n-1} \pmod{m} \\ C &= a^E \pmod{m} \end{align*} $$
which is nothing but the discrete logarithm problem, which remains famously unsolved! So how does one, in general, compute the distance $d(S_j, S_i)$ faster than brute force?
And yet...
I've found a $O(\log n)$ solution that appears to work at least in the special case of PCG, which uses $m=2^{32}$ or $2^{64}$, $a = 1 \pmod{4}$, odd $c$ and is full-period.
It's based on the fact that PCG always alternates between odd and even LCG states.
- If $S_i$ is even but $S_j$ is odd or vice-versa, then the lowest bit of $i-j$ is 1, otherwise it's 0. If it is 1, I advance the state $S_i$ by one, otherwise I leave it untouched. I then set
c = (a+1)*c
anda = a^2
. - $S_i$ and $S_j$ are now congruent modulo 2. I examine now the second rightmost bit in both $S_i$ and $S_j$: If they mismatch, I advance the state $S_i$ using the current $a$ and $c$, otherwise I leave it untouched. I then again set
c = (a+1)*c
anda = a^2
. - $S_i$ and $S_j$ are now congruent modulo 4. I examine now the third rightmost bit in both $S_i$ and $S_j$: If they mismatch, I advance the state $S_i$ using the current $a$ and $c$, otherwise I leave it untouched. I then again set
c = (a+1)*c
anda = a^2
. - ... Repeat until $S_i = S_j$.
This ad-hoc algorithm appears to work for every case I've thrown at it so far. But why, when the discrete logarithm has such a reputation for difficulty?
uint64_t lcg64Diff(const LCG64* Ss, const LCG64* Se){
uint64_t a = LCG64_a,
c = LCG64_c,
p = 1,
Z = Ss->S,
D = 0;
while(Z != Se->S){
if((Z^Se->S) & p){
Z = a*Z + c;
D += p;
}
c *= a+1;
a *= a;
p <<= 1;
}
return D;
}