Unlike the Sieve of Eratosthenes, which finds all primes in a range, Miller-Rabin is used to check if a single, specific number is prime. It is a probabilistic test, meaning it can tell you for certain if a number is composite, but it can only tell you that a number is “probably” prime.
It is the industry standard for generating large primes (e.g., for RSA encryption) because it is computationally efficient even for numbers with hundreds of digits.
The Mathematical Basis
It is based on an extension of Fermat’s Little Theorem, which states that if is prime, then for any a where
Miller-Rabin goes further by checking for “non-trivial square roots of unity.” In a prime field, the only square roots of are 1 and -1 (which is ). If we find any other square root of 1, the number is definitely composite.
How the Algorithm Works
To test an odd number :
- Decomposition: Write , where d is odd. (Essentially, you keep dividing by 2 until you can’t anymore).
- Witness Picking: Pick a random integer a (the “witness”) in the range [2, n-2].
- Initial Compute: Compute .
- If passes this round. It is “probably prime.”
- Squaring Loop: If the previous step didn’t pass, square x repeatedly s-1 times:
- Compute x = .
- If , passes. It is “probably prime.”
- If (without having hit first), it’s a non-trivial square root of unity Definitely Composite.
- Failure: If the loop finishes and you never hit n-1, the number is Definitely Composite.
Operational Reality & Trade-offs
1. The Error Rate
Each round with a different random a reduces the probability that a composite number is misidentified as prime. The probability of a composite number passing k rounds is less than . With 40 rounds, the chance of error is lower than the chance of a cosmic ray flipping a bit in your CPU and causing a crash.
2. Deterministic Miller-Rabin
For smaller numbers, we don’t need randomness. It has been proven that if you test a specific set of bases, the test becomes 100% deterministic:
- If , you only need to test a = 2, 7, 61.
- If , you only need to test
3. Complexity
The time complexity is , where k is the number of bases tested. This is incredibly fast compared to trial division or sieving for a single large number.
Summary Comparison
| Feature | Sieve (Basic/Segmented) | Miller-Rabin |
|---|---|---|
| Goal | Find all primes in a range | Check one specific number |
| Certainty | 100% Deterministic | Probabilistic (by default) |
| Memory | (Segmented) | O(1) |
| Speed for 10^{12} | Very Slow (must sieve all) | Instant (modular exponentiation) |
| Best use case | Pre-computing prime tables | Generating huge crypto keys |
C program implementation
To implement Miller-Rabin for 64-bit integers in C, the biggest challenge is overflow during modular multiplication. When you calculate , if both a and b are near , the result exceeds the capacity of a uint64_t.
I have used __int128_t (a GCC/Clang extension) to handle these intermediate products safely. I have also used a deterministic set of bases, meaning this program is 100% accurate for any number up to ; it is not probabilistic for this range.
miller_rabin.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef __uint128_t uint128;
// Modular exponentiation: (base^exp) % mod
uint64_t power(uint64_t base, uint64_t exp, uint64_t mod) {
uint64_t res = 1;
base %= mod;
while (exp > 0) {
if (exp % 2 == 1) res = (uint64_t)((uint128)res * base % mod);
base = (uint64_t)((uint128)base * base % mod);
exp /= 2;
}
return res;
}
// The Miller-Rabin test for a specific witness 'a'
int miller_test(uint64_t n, uint64_t a) {
if (a % n == 0) return 1; // a is a multiple of n, not a useful witness
uint64_t d = n - 1;
int s = 0;
while (d % 2 == 0) {
d /= 2;
s++;
}
uint64_t x = power(a, d, n);
if (x == 1 || x == n - 1) return 1;
for (int r = 1; r < s; r++) {
x = (uint64_t)((uint128)x * x % n);
if (x == n - 1) return 1;
}
return 0; // Definitely composite
}
// Deterministic primality check for uint64_t
int is_prime(uint64_t n) {
if (n < 2) return 0;
if (n == 2 || n == 3) return 1;
if (n % 2 == 0) return 0;
// Deterministic bases for n < 2^64
// These bases are proven to be sufficient for all 64-bit integers
uint64_t bases[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
int num_bases = sizeof(bases) / sizeof(bases[0]);
for (int i = 0; i < num_bases; i++) {
if (n == bases[i]) return 1;
if (!miller_test(n, bases[i])) return 0;
}
return 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <number>\n", argv[0]);
return 1;
}
uint64_t n = strtoull(argv[1], NULL, 10);
if (is_prime(n)) {
printf("%llu is PRIME\n", (unsigned long long)n);
} else {
printf("%llu is COMPOSITE\n", (unsigned long long)n);
}
return 0;
}Code language: C++ (cpp)
Compilation and Execution
gcc -O3 miller_rabin.c -o miller_rabin
./miller_rabin 18446744073709551557 # Largest 64-bit prime
Engineering Breakdown:
__uint128_t: This is the most pragmatic way to implement Miller-Rabin in C. Without it, you would have to implement a custommul_modfunction that performs multiplication via addition (similar to binary exponentiation), which is significantly slower.- Deterministic Bases: I used the bases . This specific set is proven to be deterministic for all . You don’t need to worry about “probably prime” vs “definitely prime” for standard 64-bit unsigned integers.
- Complexity: The time complexity is , where k is the number of bases (12 in this case). The check is virtually instantaneous even for the largest possible
uint64_t. - Edge Case Handling:
- : Not prime.
- : Prime.
- Even numbers > 2: Composite.
- Base a being a multiple of n: Handled in
miller_test().