Number Theory & Algebra contents
Binary Exponentiation by Factoring
Compute a·x^y modulo 2^32 (x odd) with O(d) additions and one multiplication, using a discrete logarithm in the group modulo a power of two.
Read first: Binary Exponentiation, Discrete Logarithm
This is a specialised trick, worth knowing as an example of how group structure yields algorithms. The task:
compute for an odd , with (typically or : the natural wrap-around arithmetic of machine integers).
Binary exponentiation needs multiplications. Using the structure of the multiplicative group modulo , we can do it with additions and bit operations and a single multiplication by .
The group modulo
Every number can be written as a power of a fixed base with :
The exponent is a discrete logarithm. Then : we need (a logarithm), one multiplication by , and then an exponential . The trick is to make both of these cheap.
If replace and , since and .
We work with (modulo ) rather than ; it makes the bit structure of the table cleaner.
Computing by peeling off factors
Every factors as
so . If we precompute the table for , then needs at most table additions.
To find the factors: repeatedly multiply by for those bits (increasing) at which currently has a 1. Since and is odd, this clears bit and leaves the lower bits untouched. After the loop , so we found the factorization of , and .
The table
Computing for the numbers can be done by a bit-by-bit search (a specialisation of the Pohlig-Hellman algorithm): the group generated by has order , so we can determine the bits of from the lowest one upward by checking whether a suitable power of the remaining quantity is .
D = 32
MASK = (1 << D) - 1
def dlog5(y, d=D):
"""Exponent L with 5^L = y (mod 2^d), for y = 1 (mod 4). Found bit by bit."""
M = 1 << d
L, cur = 0, y % M
for i in range(d - 2):
if pow(cur, 1 << (d - 3 - i), M) != 1: # bit i of L is set
L |= 1 << i
cur = cur * pow(pow(5, 1 << i, M), -1, M) % M
return L
assert all(pow(5, dlog5(y), 1 << D) == y for y in (5, 9, 17, 33, 1 + (1 << 20), 0x12345671))
# The published algorithm uses the base b = 0x1998df85 (b = 5 mod 8) instead of 5.
B = 0x1998DF85
L5_of_B = dlog5(B) # b = 5^L5_of_B, and L_b(y) = L_5(y) / L_5(b)
inv = pow(L5_of_B, -1, 1 << (D - 2))
TABLE = [0, 0] + [(4 * dlog5((1 << n) + 1) * inv) & MASK for n in range(2, D)]
published = [
0x00000000, 0x00000000, 0xd3cfd984, 0x9ee62e18, 0xe83d9070, 0xb59e81e0, 0xa17407c0, 0xce601f80,
0xf4807f00, 0xe701fe00, 0xbe07fc00, 0xfc1ff800, 0xf87ff000, 0xf1ffe000, 0xe7ffc000, 0xdfff8000,
0xffff0000, 0xfffe0000, 0xfffc0000, 0xfff80000, 0xfff00000, 0xffe00000, 0xffc00000, 0xff800000,
0xff000000, 0xfe000000, 0xfc000000, 0xf8000000, 0xf0000000, 0xe0000000, 0xc0000000, 0x80000000,
]
assert TABLE == published # we regenerated the table from first principlesOur independently computed table equals the constants published in the original article.
Note the pattern in the upper half: for we have , and the table entries there are simply (the constants 0xffff0000, 0xfffe0000, ... are in two's complement):
assert all(TABLE[n] == (-(1 << n)) & MASK for n in range(16, 32))The functions
def mbin_log_32(r, x):
"""r + 4*L(x) mod 2^32, for x = 1 (mod 4)."""
for n in range(2, D):
if x & (1 << n):
x = (x + (x << n)) & MASK
r = (r - TABLE[n]) & MASK
return r
def mbin_exp_32(r, x):
"""r * b^(x/4) mod 2^32 (x a multiple of 4 given as a 4L value)."""
for n in range(2, D):
if x & (1 << n):
r = (r + (r << n)) & MASK
x = (x - TABLE[n]) & MASK
return r
def mbin_power_odd_32(rem, base, exp):
"""rem * base^exp mod 2^32 for odd base."""
if base & 2: # base = 3 (mod 4): use -base
base = (-base) & MASK
if exp & 1:
rem = (-rem) & MASK
return mbin_exp_32(rem, (mbin_log_32(0, base) * exp) & MASK)
import random
random.seed(1)
for _ in range(2000):
a = random.getrandbits(32)
x = random.getrandbits(32) | 1
y = random.getrandbits(random.randint(1, 40))
assert mbin_power_odd_32(a, x, y) == a * pow(x, y, 1 << 32) % (1 << 32)
assert mbin_power_odd_32(1, 3, 5) == 243
assert mbin_power_odd_32(7, 0xFFFFFFFF, 3) == (7 * pow(0xFFFFFFFF, 3, 1 << 32)) & MASKThe loops contain no multiplications: only additions, shifts and mask operations, plus the single product mbin_log_32(0, base) * exp.
Halving the loop
Because the table entries for are just , the loop can stop at ; the remaining high bits of are handled with a single subtraction (for the logarithm) or a single multiplication (for the exponential) instead of more iterations:
def mbin_log_32_fast(r, x):
for n in range(2, 16):
if x & (1 << n):
x = (x + (x << n)) & MASK
r = (r - TABLE[n]) & MASK
return (r - (x & 0xFFFF0000)) & MASK
def mbin_exp_32_fast(r, x):
for n in range(2, 16):
if x & (1 << n):
r = (r + (r << n)) & MASK
x = (x - TABLE[n]) & MASK
return (r * (1 - (x & 0xFFFF0000))) & MASK
for _ in range(2000):
x = random.getrandbits(32) & ~2 | 1 # x = 1 (mod 4)
assert mbin_log_32_fast(0, x) == mbin_log_32(0, x)
e = random.getrandbits(30) << 2
assert mbin_exp_32_fast(1, e) == mbin_exp_32(1, e)The exact same code with 64-bit masks and a 64-entry table gives the 64-bit version.
Why it matters
The technique shows that the structure of the multiplicative group (cyclic times for powers of two) can turn exponentiation into arithmetic on exponents. The same ideas underlie the discrete logarithm algorithms and primitive roots. Direct uses: fast hash functions with multiplicative mixing modulo , deterministic pseudo-random jump-ahead for linear congruential generators, and proofs about .