18.21 Big Integer Concerns
Number-theoretic algorithms often look simple when written with mathematical notation.
18.21 Big Integer Concerns
Number-theoretic algorithms often look simple when written with mathematical notation. In code, the details are less forgiving. Modular multiplication can overflow. Exponentiation can allocate huge temporary values. Division may dominate runtime. Randomized algorithms may behave differently across platforms. Cryptographic code may leak information through timing.
Big integer concerns are the implementation layer beneath computational number theory. They determine whether an otherwise correct algorithm remains correct, fast, and safe when inputs grow beyond native machine words.
Problem
Given arithmetic algorithms involving large integers, implement operations such as:
a + b
a × b
a^e mod m
gcd(a,b)
a⁻¹ mod m
without overflow, excessive memory use, accidental precision loss, or avoidable performance problems.
The challenge is that mathematical integers are unbounded, while machine integers usually are not.
Fixed-Width Integers
Most systems provide fixed-width integer types.
Common examples:
| Type | Approximate Range |
|---|---|
| 32-bit unsigned | 0 to 2³² - 1 |
| 64-bit unsigned | 0 to 2⁶⁴ - 1 |
| signed 64-bit | -2⁶³ to 2⁶³ - 1 |
If a result exceeds the range, behavior depends on the language.
In Python, integers grow automatically. In C and C++, signed overflow is undefined behavior. In Go, Rust, Java, and many other languages, fixed-width integer operations wrap, panic in checked modes, or require explicit big integer types.
This matters because modular arithmetic commonly contains expressions such as:
(a × b) mod m
Even if the final residue fits in 64 bits, the product a × b may not.
Overflow Before Modulo
Consider:
a = 10^18
b = 10^18
m = 10^18 + 7
The final answer:
(a × b) mod m
is less than m, so it fits in 64 bits.
But the intermediate product:
a × b = 10^36
does not fit in 64 bits.
This is a classic source of incorrect modular arithmetic in fixed-width languages.
Safe Modular Multiplication
In Python, this is safe:
def mod_mul(a, b, m):
return (a * b) % m
In a fixed-width language, use one of these strategies:
wider intermediate type
big integer library
Montgomery multiplication
Barrett reduction
repeated-doubling multiplication
For example, in C++ with 64-bit values, __int128 is commonly used for the intermediate product.
Conceptually:
result = ((128-bit)a × b) mod m
This handles moduli up to 64 bits.
Repeated-Doubling Multiplication
When no wider type exists, multiplication can be simulated through addition and doubling.
def mod_mul_slow(a, b, m):
a %= m
b %= m
result = 0
while b > 0:
if b & 1:
result = (result + a) % m
a = (a + a) % m
b >>= 1
return result
Complexity:
O(log b)
This is slower than native multiplication, but avoids overflow.
Use it when correctness matters and no better primitive is available.
Modular Exponentiation with Safe Multiplication
Fast exponentiation depends on multiplication.
def mod_pow_safe(base, exponent, modulus):
result = 1
base %= modulus
while exponent > 0:
if exponent & 1:
result = mod_mul_slow(result, base, modulus)
base = mod_mul_slow(base, base, modulus)
exponent >>= 1
return result
This version is safe for fixed-width-style arithmetic but slower than normal multiplication.
In Python, prefer built-in pow(base, exponent, modulus).
Prefer Built-In Big Integer Operations
In Python, this is usually the best implementation:
pow(base, exponent, modulus)
It is faster and more memory-efficient than:
(base ** exponent) % modulus
because it performs modular reduction throughout exponentiation.
Use:
pow(a, -1, m)
for modular inverse when available.
Example:
print(pow(3, -1, 11))
Output:
4
This computes the inverse of 3 modulo 11.
Avoid Floating-Point Arithmetic
Number theory should usually remain integer-only.
Avoid:
int(n ** 0.5)
for large n.
Floating-point square roots can round incorrectly.
Use:
from math import isqrt
limit = isqrt(n)
This returns the exact integer floor of the square root.
Example:
from math import isqrt
def is_square(n):
r = isqrt(n)
return r * r == n
This is exact for arbitrarily large integers.
Big Integer Cost Model
For native integers, we often treat arithmetic as constant time.
For big integers, arithmetic cost depends on the number of machine words.
Let:
L = number of bits in n
Then common operations have approximate costs:
| Operation | Rough Cost |
|---|---|
| Addition | O(L) |
| Comparison | O(L) |
| Multiplication, classical | O(L²) |
| Division | O(L²) |
| GCD | O(L² log L), implementation-dependent |
| Modular exponentiation | O(log e) multiplications |
Real big integer libraries use faster algorithms for large inputs, but the key point remains: arithmetic itself is no longer free.
This affects algorithm selection.
Memory Allocation
Big integer operations allocate temporary objects.
The expression:
x = (a * b + c * d) % m
may allocate intermediate products before reduction.
When performance matters, reduce aggressively:
x = (a * b) % m
x = (x + (c * d) % m) % m
In Python, this still allocates, but it keeps values smaller. In lower-level languages, specialized modular arithmetic routines can avoid repeated allocation.
Sign Handling
Big integer libraries may represent negative values differently internally, but APIs generally preserve mathematical semantics.
Still, modular arithmetic with negative numbers should be normalized.
def normalize_mod(x, m):
return x % m
In Python:
-3 % 5 == 2
In some languages, the remainder may keep the sign of the dividend.
A portable normalization pattern is:
((x % m) + m) % m
Use this when porting algorithms across languages.
GCD and Extended GCD
GCD is usually highly optimized in standard libraries.
In Python:
from math import gcd
print(gcd(123456789, 987654321))
For modular inverses, Extended Euclid remains useful:
def extended_gcd(a, b):
old_r, r = a, b
old_s, s = 1, 0
old_t, t = 0, 1
while r:
q = old_r // r
old_r, r = r, old_r - q * r
old_s, s = s, old_s - q * s
old_t, t = t, old_t - q * t
return old_r, old_s, old_t
For production code, prefer library primitives when available.
Random Big Integers
Randomized algorithms such as Miller-Rabin and Pollard's Rho require random bases or starting values.
For non-cryptographic algorithms:
import random
may be enough.
For cryptographic code, use a cryptographically secure generator:
import secrets
x = secrets.randbelow(n)
Do not use ordinary pseudorandom generators for key generation, primality generation, or security-sensitive randomness.
Timing Side Channels
In cryptographic code, the execution time of arithmetic operations may reveal information about secret values.
Examples:
branching on secret bits
variable-time modular exponentiation
early exits based on secret data
table lookups indexed by secrets
The textbook fast exponentiation loop:
if exponent & 1:
result = result * base % modulus
branches on exponent bits. That is acceptable for ordinary algorithms but problematic when the exponent is secret.
Cryptographic libraries use constant-time algorithms or blinding techniques.
Do not implement cryptographic primitives from scratch unless you understand these constraints.
Big Integer Libraries
Different languages provide different big integer support.
| Language | Common Big Integer Support |
|---|---|
| Python | built-in int |
| Java | BigInteger |
| Go | math/big |
| Rust | num-bigint, crypto-specific crates |
| C++ | Boost.Multiprecision, GMP |
| C | GMP, OpenSSL BIGNUM |
| JavaScript | BigInt |
For serious performance, mature libraries such as GMP are often much faster than simple language-level implementations.
Performance Pattern: Reduce Early
When computing expressions modulo m, reduce after each operation.
Prefer:
x = (a % m) * (b % m) % m
over:
x = (a * b) % m
when a and b may already be huge.
For Python this is mostly a performance issue. In fixed-width languages it may also be a correctness issue.
Performance Pattern: Batch Precomputation
If an algorithm repeatedly needs modular inverses, factorials, powers, or prime tables, precompute once.
Examples:
factorial table
inverse factorial table
smallest prime factor table
powers of a generator
Montgomery constants
Precomputation trades memory for speed and often turns repeated logarithmic operations into constant-time lookups.
Performance Pattern: Choose the Right Modulus
Some moduli are friendlier than others.
Prime moduli simplify inverse computations.
NTT-friendly primes support fast polynomial multiplication.
Powers of two enable bit masking:
x mod 2^k
can be computed as:
x & (2^k - 1)
provided x is nonnegative and the language's integer model is compatible.
Cryptographic moduli are selected according to stricter security and algebraic requirements.
Common Mistakes
Computing Huge Powers Directly
Avoid:
(base ** exponent) % modulus
when exponent is large.
Use:
pow(base, exponent, modulus)
Assuming Multiplication Cannot Overflow
Even if the final result is reduced modulo m, the intermediate product can overflow.
Using Floating-Point Square Roots
Use isqrt, not floating-point approximation.
Using Non-Crypto Randomness for Crypto
Use secrets or a cryptographic library for key material.
Writing Crypto from Scratch
Number-theoretic code for learning is different from production cryptography.
Side-channel resistance, parameter validation, padding, randomness, and protocol design all matter.
Testing
For modular multiplication, compare safe and direct versions in Python:
def test_mod_mul_slow():
cases = [
(10**18, 10**18, 10**18 + 7),
(2**63 - 1, 2**63 - 5, 2**61 - 1),
(123456789, 987654321, 1000000007),
]
for a, b, m in cases:
assert mod_mul_slow(a, b, m) == (a * b) % m
For modular exponentiation:
def test_mod_pow_safe():
for base in range(1, 100):
for exponent in range(0, 100):
for modulus in range(1, 50):
assert mod_pow_safe(base, exponent, modulus) == pow(base, exponent, modulus)
For square roots:
from math import isqrt
def test_is_square():
for n in range(10000):
r = isqrt(n)
assert (r * r == n) == is_square(n)
These tests are simple but catch the most common arithmetic errors.
Takeaway
Big integer concerns are where mathematical algorithms meet machine limits. Modular arithmetic must avoid overflow before reduction, exponentiation should use modular methods rather than full powers, square-root bounds should use exact integer arithmetic, and cryptographic code must consider randomness and timing behavior. Languages with built-in big integers remove many correctness hazards, but they do not remove performance costs. Reliable number-theoretic software depends on choosing the right arithmetic representation, reducing early, using library primitives where possible, and testing edge cases aggressively.