Fast-Primes is a hyper-optimized, standalone
Designed to compute massive primes without relying on standard libraries, this experiment bypasses libc completely. It issues direct x86-64 Linux system calls for memory management (mmap) and I/O, yielding maximum performance and total control over execution. Instead of relying on brute-force sieving, it implements an analytic number theory pipeline centered around Lehmer's Formula and the Prime Number Theorem.
For a more in-depth explanation of the mathematical framework, algorithmic design, and optimization techniques, read the accompanying writeup:
Beyond the Billionth Prime: Pushing Bare-Metal LLVM to the Limit
To achieve extreme sub-linear time complexity, the program avoids counting primes from zero. The execution pipeline is divided into distinct phases:
-
The Fast-Path (Small
$n$ ): If$n \le 600{,}000$ , the program instantly returns the result from the precomputed prime table built during the startup sieve, which covers all primes up to$10^7$ . -
The Analytical Guess: Uses the inverse Logarithmic Integral to estimate the numerical value of the
$n$ -th prime. - The Exact Count: Uses Lehmer's Formula to calculate exactly how many primes exist below a padded lower bound of the guess.
-
The Localized Sieve: Uses a dynamically allocated Sieve of Eratosthenes spanning the calculated error margin to scan forward to the exact target
$n$ .
Unlike traditional block-sieve implementations of Lehmer's algorithm bounded by
-
Time Complexity: The worst-case bound for the state space of
$\phi(x, a)$ is$O\left(\frac{x^{3/4}}{\log x}\right)$ . However, the heavy use of memoization via open-addressed hash tables prevents identical sub-states from being recomputed, bringing the practical performance closer to$O(x^{2/3})$ on average for massive inputs. -
Space Complexity:
$O\left(\frac{x^{1/2}}{\log x}\right)$ , dominated by the gigabytes of hash table cache dynamically allocated viammapto store the$\phi(x,a)$ and$\pi(x)$ recursion branches.
Because raw execution time is heavily dependent on CPU microarchitecture, clock speed, and L3 cache sizes, the following benchmarks focus on relative algorithmic throughput measured in Operations Per Second (Ops/Sec) and relative speedup multipliers.
To provide context for the absolute time metrics, the benchmarks were generated on the following hardware:
- CPU: Intel Core i5-12450H
- L3 Cache: 12 MB
- OS: CachyOS Linux x86-64
- Compiler: LLVM
optandllcversion 22.1.5
The table below compares the performance of Fast-Primes against several baseline algorithms from [SheafificationOfG/QueenJewels](https://github.com/SheafificationOfG/QueenJewels). The benchmarking harness dynamically searches for the highest
(Note: The metrics are derived from the harness in SheafificationOfG/QueenJewels, which was adapted to benchmark the algorithms in this project. The repository is attached in the references/QueenJewels directory).
| Algorithm | Prime number index ( |
Prime number ( |
Relative Speed |
|---|---|---|---|
| Naïve Iter. | 9_405 |
97_859 |
1.0x |
| Sqrt Iter. | 266_124 |
3_740_981 |
28.3x |
| Miller-Rabin Iter. | 337_620 |
4_833_739 |
35.8x |
| Sieve of Pritchard | 3_898_440 |
66_041_489 |
414.5x |
| Sieve of Erat., sqrt | 7_823_791 |
138_344_399 |
831.8x |
| Packed Sieve of Pritch. | 21_306_021 |
399_403_699 |
2265.4x |
| Segm. Sieve of Erat. (byte-aligned) | 27_269_352 |
518_329_457 |
2899.5x |
| WF Segm. Sieve of Erat. | 36_111_874 |
697_123_187 |
3839.6x |
| Legendre's Formula | 279_151_390 |
5_991_131_831 |
29681.1x |
| Lehmer Sieve (fast-primes) | 23_762_554_725 |
620_490_072_817 |
2526587.4x |
(Fast-Primes achieves an ~85.1x performance multiplier directly over Legendre's baseline formula).
Below is the visualized performance curve.
By the Prime Number Theorem,
Algorithmic nuances:
- Instead of the classical integral definition of
$\text{li}(x)$ , the program computes the asymptotic expansion series:
The series sum loop explicitly terminates when the absolute value of the current term drops below
- The Newton iteration is capped at a maximum of 8 iterations, with an early exit if the absolute value of the correction step is
$< 0.5$ .
Once the estimate
Mathematically, Lehmer's formula is written using 1-based prime indexing (
Implementation Note (0-Based Array Translation): The program stores primes in a 0-based memory array (
primes[0] = 2). This means every 1-based mathematical index$k$ corresponds to the 0-based code variablek - 1. Concretely:
- The outer sum's lower bound
$i = a+1$ (1-based) maps to the loop initializer%i = %ain the code.- The inner sum's lower bound
$j = i$ (1-based) maps to the loop initializer%j = %iin the code.- The subtracted term
$(j-1)$ in the formula (1-based) maps to the code subtracting%jdirectly (0-based) — these are the same numerical value.In both loops,
primes[i](0-based) retrieves$p_{i+1}$ in 1-based notation, so the 0-based memory access and 1-based formula are fully consistent.
The computational bottleneck is
Instead of using the standard Legendre recursion
Implementation Note (0-Based Array Translation): In the code, the loop variable
iis 0-based and runs from6toa - 1. At each step,primes[i](0-based) retrieves$p_{i+1}$ in 1-based notation, and the recursive call passesias the second argument — corresponding to the 1-based index$i$ shown in the formula above. The formula and the code are therefore fully consistent.
To compute the base case
For
Because
To guarantee the target prime is captured, the program:
- Computes a safe margin:
$\text{margin} = 25\sqrt{\hat{x}}$ . -
Subtracts this margin from the estimate to create a safe lower base:
$\text{base} = \hat{x} - \text{margin} + 1$ . - Calls Lehmer's formula to find the exact count
$C = \pi(\text{base} - 1)$ , and derives the remaining target$r = n - C$ . - Allocates a byte-sieve spanning the interval
$[\text{base},; \text{base} + 3 \times \text{margin})$ and marks composites using the pre-cached base primes. - Scans linearly forward, decrementing
$r$ for each unmarked (prime) byte. When$r = 0$ , the$n$ -th prime has been found.
Because this code is written in pure LLVM IR, it employs several low-level optimizations:
-
No
libc/ True Bare-Metal Entry: The program defines a naked_startfunction (notmain). It executes inline assembly to manually readargcandargvdirectly off the stack pointer%rsp. It terminates using a rawsyscall(number60forsys_exit), skipping all standardatexithandlers entirely. -
Knuth Multiplicative Hashing: Both hash tables (for
$\phi$ and for$\pi$ ) use open addressing with linear probing. The hash function uses Knuth's multiplicative constant-7046029254386353131— which is$2^{64} \times (\sqrt{5}-1)/2$ in unsigned two's complement — to scatter keys uniformly across the table. -
The
fyl2xx87 Trick: To compute the natural logarithm required for the Logarithmic Integral, the program issues the x86fyl2xfloating-point instruction with$y = \ln(2) \approx 0.693147$ . This computes$y \cdot \log_2(x) = \ln(x)$ in a single hardware step, avoiding the need for a software math library. -
128-Bit Return Type: The master
prime()function returns ani128type. While the current memory allocation bounds the practical computation, the underlying pipeline is intrinsically capable of returning prime values beyond the 64-bit integer limit. -
Input Validation: The program rejects
$n \le 0$ before allocating any memory, preventing unsigned integer underflow when converting the 1-based user input to the 0-based internal index. Any non-numeric or zero argument causes an early exit with a descriptive error message tostderr.
This experiment requires the LLVM toolchain (llvm-link, opt, llc, and lld).
Build the executable:
The Makefile exposes two build targets:
make generic # Default. Compiles without -march tuning; maximally portable.
make native # Compiles with -march=x86-64. Faster on x86-64 hosts.
make # Alias for generic.To target a specific microarchitecture, override MARCH_FLAG directly:
make native MARCH_FLAG=-march=znver4Run the calculator:
Pass the target
./bin/fast-primes 1_000_000_000The result is printed to stdout with underscore digit separators (e.g., 22_801_763_489). Any usage error is printed to stderr and the program exits with code 1.
- Lehmer, D. H. (1959). On the exact number of primes less than a given limit. Illinois Journal of Mathematics, 3(3), 381–388. DOI: 10.1215/ijm/1255455259
- Lagarias, J. C., Miller, V. S., & Odlyzko, A. M. (1985). Computing $\pi(x)$: The Meissel-Lehmer method. Mathematics of Computation, 44(170), 537–560. DOI: 10.1090/S0025-5718-1985-0777285-5
- Hadamard, J. & de la Vallée Poussin, C. J. (1896). The Prime Number Theorem. Wikipedia
- Crandall, R., & Pomerance, C. (2005). Prime Numbers: A Computational Perspective. Springer. DOI: 10.1007/0-387-28979-8
- Forbes, T. (2002). Review: Prime numbers: A computational perspective, by Richard Crandall and Carl Pomerance. The Mathematical Gazette, 86, 552–554. DOI: 10.2307/3621190
-
Sheafification of G: Inspiration for this project was drawn from the YouTube video One second to find the BILLIONth PRIME. The bare-metal x86-64 Linux system call wrappers (
src/sys/amd64_rt.ll,src/sys/amd64_sys.ll) are from the GitHub repository SheafificationOfG/QueenJewels. A reference to the repository is included in thereferences/QueenJewelsdirectory. - haskallcurry/primes: The implementation of the Meissel-Lehmer algorithm in the haskallcurry/primes repository served as a foundational reference for this experiment.
