https://hnlyman.github.io/pages/prime32_I.html Back to home Generating All 32-Bit Primes (Part I) This article documents my quest to write a C program targeting Linux that generates all prime numbers that fit in a 32-bit unsigned int (uint32_t) as quickly as possible. In particular, the program should write all 32-bit primes to a file (which, in all my implementations, is called PRIMES) in binary, so that every 4 bytes of the file stores one primes number, with the bytes ordered in a little-endian fashion. In hex, the file should start out: 02 00 00 00 03 00 00 00 05 00 00 00 07 00 00 00, and the correct SHA-256 hash for the file turns out to be: 272eb05aa040ba1cf37d94717998cbbae53cd669093c9fa4eb8a584295156e15. The algorithm used should able to correctly generate primes up to an arbitrarily large limit (within reasonable hardware constraints). It should not assume the primality of any number larger than 2 without first verifying it. It should not use a huge amount of memory--1GB should be plenty. Trial Division The simplest and most obvious way to check a number's primality is trial division. Given a target integer n, where n \ge 3, we check if n is divisible by each prime number less than or equal to its square root. If and only if it is not divisible by any of them, we can conclude that n is prime. In C we could implement trial division like this: /// Returns `true` iff `n` is a prime number. /// /// `primes`: an array of prime numbers in order, starting from 2, skipping /// none, and going at least up to the square root of `n`. bool is_prime(uint32_t n, uint32_t* primes) { for (size_t i=0; ; i++) { uint32_t p = primes[i]; if (n % p == 0) { return false; } // Comparison against 0xFFFF prevents overflow in `p*p` if (p >= 0xFFFF || p*p >= n) return true; } } In the worst case, carrying out this algorithm requires \pi(\sqrt n) divisions, where \pi is the prime-counting function. (That is, \pi(x) is the number of primes less than or equal to n). Since \pi(x) is asymptotically equivalent to \frac{x}{\ln x} ^[1], the trial division algorithm runs in O(\sqrt {n} / \ln \sqrt n) = O(\sqrt{n} / \ln n) time. Trial division can be used to generate all the prime numbers up to a limit N \ge 3 as follows: we create a growing list of the primes in order, which is initialized to {2}. Then we go through all the integers from 3 to N, checking the primality of each one and adding it to the end of the list if it is found to be prime. In C, for N=2^ 32-1: uint32_t* primes = (uint32_t*) malloc(203739216 * sizeof(uint32_t)); primes[0] = 2; size_t p_idx = 1; // index of the next element to be added to `primes` for (uint32_t n=3; n<=0xFFFFFFFF; n+=2) { if ( is_prime(n, primes) ) { primes[p_idx] = n; p_idx++; } // Prevents overflow in `n+=2` if (n == 0xFFFFFFFF) break; } ^[2] This algorithm calls is_prime O(N) times, so its time complexity in the worst case is within O(N \sqrt N / \ln N). The array primes can be written to a file as follows: FILE* prime_file = fopen("PRIMES", "wb"); fwrite(primes, 4, p_idx, prime_file); Now we have everything we need to write a full program meeting the specifications laid out in this article's introduction. The implementation found here runs in about 24m20s of user time on my system.^[3] Wheel Factorization Some numbers are obviously not prime, and we are wasting our time by even checking. For instance, all even numbers greater than 2 (or, in other words, all integers greater than 2 and congruent to 0 modulo 2) are clearly not prime. The implementation of the sequential trial division algorithm already takes advantage of this fact, incrementing the for loop with n+=2, instead of n++, to skip all even numbers (n is always greater than 2). A well-known result is the fact that all primes greater than 3 are congruent to either 1 or 5 mod 6. This is easy to prove by cases: any number congruent to 0, 2, or 4 mod 6 is divisible by 2, and any such number greater than 2 is therefore composite; and any number congruent to 3 mod 6 is divisible by 3, and any such number greater than 3 is therefore composite. Less well known is the fact that all primes greater than 5 are congruent to 1, 7, 11, 13, 17, 19, 23, or 29 mod 30, but this can be proved by cases in a very similar fashion: 30 is 2x3x5, so it is easy to eliminate all the possible remainders mod 30 which imply divisibility by 2, 3, or 5. Let's generalize. Suppose we know the first k primes, which we shall call p_1, p_2, p_3, \cdots, p_k. Let k\# be the primorial of k. That is, the product of the first k primes, or p_1 \times p_2 \times p_3 \ times \cdots \times p_k. Now, take the sequence 0, 1, 2, 3, \cdots, (k\#-1), and delete any numbers divisible by any of p_1, p_2, p_3, \ cdots, p_k. All prime numbers greater than p_k will be congruent modulo k\# to one of the numbers still remaining in the sequence. Here is a diagram of a 'wheel' of size 3\# = 30. The numbers in each 'spoke' form a congruence class modulo 30. The darkened spokes contain exactly those numbers which are divisible by 2, 3, or 5, or, in other words, are not coprime to 30. Clearly, no primes greater than 5 will be found in the darkened spokes. The white spokes contain all natural numbers coprime to 30, and may be called the coprime spokes. [wheel_30] ^[4] Note that the properties 'not divisible by any of the first k primes', 'coprime to k\#', and 'found on a coprime spoke of the wheel of size k\#' are all equivalent. We can represent a wheel in C with the following struct: struct Wheel { size_t size; /// Numbers with these remainders modulo `size` are coprime to `size`. uint32_t* coprime_spokes; /// The number of elements in `spokes` size_t n_spokes; }; And initialize it as follows: /// Initialize a `struct Wheel` using the first `k` prime numbers. /// /// `primes`: an array of size at least `k` whose first `k` elements are the /// first `k` primes in order struct Wheel wheel_init(uint32_t* primes, size_t k) { struct Wheel wheel; wheel.size = 1; for (size_t i=0; i