zs3.me

A Prime Number Generator Benchmark

Revision 50
© 2013-2023, 2026 by Zack Smith. All rights reserved.

TL;DR

This is my benchmark that generates prime numbers using unsigned integer division instructions and avoidance of divisions.

It maintains an array that contains the resulting prime numbers, which are also used to factor prospective prime numbers.

What is special about my algorithm is that I maintain prime counters in CPU registers that let me avoid checking about 87% of numbers for primeness.

In addition, in my newer implementations, I use speculative execution to effectively perform 2 divisions simultaneously, on CPUs that allow that.

Because this algorithm is heavily dependent on the division operation, moreso than memory accesses, it is effectively a benchmark of a CPU's division operation, particularly its speculative execution, and secondarily memory access speed.

For enjoyment I've ported the core assembly language routines to multiple instruction set architectures including: x64, i386, aarch64, aarch32, and riscv64, and on x64 I've added AVX support.

Key findings:

  1. The Apple Silicon processors are considerably more efficient than X86 processors.

  1. All CPUs that allow using speculative execution to effectively parallelize divisions
  2. are faster than those that don't.

  1. While Apple Silicon is ARM-based, other ARM-based CPUs are less efficient than it e.g.
  2. the Raspberry Pi 4's Broadcom CPU and a couple Qualcomm Snapdragon CPUs that I tried.

  1. Because division instructions are very time-consuming, this algorithm is
  2. much slower than the Sieve of Eratosthenes, despite my design focusing on avoiding divisions.

The algorithm

Instead of eliminating every single multiple of every prime using a bit array, as the Eratosthenes algorithm does, I maintain counters for multiples of the lower primes in the registers.

As the program proceeds from lower into higher candidate numbers, the counters permit skipping over multiples of primes without maintaining a bit array.

For instance, my counter3 is cycled from 0 to 1 to 2 and when we hit 3, it's rolled over to 0. Every time roll over a prime counter, we know that the current candidate number is a multiple of that prime, therefore not prime itself.

Using these counters lets me avoid checking about 87% of all candidate numbers when the counters are located in the CPU's main registers.

The exact percentage varies a little depending on the implementation and how many CPU registers are free to be used for counters e.g. there are more on x64 than on i386.

I also have an implemention that uses the x86 AVX vector registers which has even more counters, but there is a diminishing return, because as one adds more counters the efficiency increases less and less.

What makes this algorithm slower than Eratosthenes is the primeness check, which is very expensive, since it require repeated unsigned integer divisions. Checking a number for primeness entails dividing known primes into it in an attempt to factor it.

A common optimization is to only divide primes up to 1+sqrt(number), because if a larger factor than that exists, then a smaller one must as well and it would have already been encountered in an earlier division.

This algorithm would probably work best in conjunction with an FPGA with let's say 1000 counters to increase the efficiency closer to a theoretical maximum avoidance rate.

One advantage of this algorithm is that it's memory efficient, since there is no bit array as with Eratosthenes; there is only the primes array.

Here is my pseudocode for the my algorithm, checking against the lower 9 primes:

 function generate_primes
 {
  array primes[] = { 2,3,5,7,11,13,17,19,23 }
  array of bytes prime_counters[9] = { 0,0,0,0,0,0,0,0,0 }
  int n_primes = 9
  int n = 0
  while (always) {
   ++n
   for i = 1 to 8 {
    prime_counters[i]++
   }
   bool skip = false
   if prime_counters[0] >= 3 {
    prime_counters[0] = 0
    skip = true
   }
   if prime_counters[1] >= 5 {
    prime_counters[1] = 0
    skip = true
   }
   if prime_counters[2] >= 7 {
    prime_counters[2] = 0
    skip = true
   }
   if prime_counters[3] >= 11 {
    prime_counters[3] = 0
    skip = true
   }
   if prime_counters[4] >= 13 {
    prime_counters[4] = 0
    skip = true
   }
   if prime_counters[5] >= 17 {
    prime_counters[5] = 0
    skip = true
   }
   if prime_counters[6] >= 19 {
    prime_counters[6] = 0
    skip = true
   }
   if prime_counters[7] >= 23 {
    prime_counters[7] = 0
    skip = true
   }
   if (skip) continue
   if (n is even) then continue
   if (is_prime (n, primes, n_primes)) {
    primes[n_primes++] = n
    println ("%llu is prime", n)
   }
  }
  return primes
 }

Results

100 million primes

CPU frequency CPU name and RAM type Total primes generated Data size Implementation Duration Efficiency*
4.41 GHz Apple Mac Mini M4, MacOS, 16GB LPDDR5-7500 100 million primes 64-bit aarch64 asm
28div
3.10 minutes 7.31 M
4.464 GHz Apple MBA-13 M5, MacOS, 24GB LPDDR5X-8533 100 million primes 64-bit aarch64 asm
28div
3.10 minutes 7.23 M
4.608 GHz Apple MBP-14 M5, MacOS, 16GB LPDDR5X-8533 100 million primes 64-bit aarch64 asm
28div
3.00 minutes 7.23 M
3.2 GHz Apple MBA-13 M1, Asahi Linux, 8GB LPDDR4X-4266 100 million primes 64-bit aarch64 asm
28div
4.32 minutes 7.23 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 48GB LPDDR5-8533 100 million primes 64-bit aarch64 asm
28div
3.07 minutes 7.22 M
4.06 GHz Apple MBP-14 M3, MacOS, LPDDR5-6400 100 million primes 64-bit aarch64 asm
28div
3.43 minutes 7.18 M
4.06 GHz Apple MBP-14 M3, MacOS, LPDDR5-6400 100 million primes 64-bit aarch64 asm
4div
3.45 minutes 7.14 M
3.5 GHz Apple MBA-15 M2, Linux, LPDDR5-6400 100 million primes 64-bit aarch64 asm
4div
4.00 minutes 7.1 M
4.41 GHz Apple MBA-13 M4, MacOS, 24GB LPDDR5-7500 100 million primes 64-bit aarch64 asm
28div
3.27 minutes 6.93 M
4.608 GHz Apple MBP-14 M5, MacOS, 16GB LPDDR5X-8533 100 million primes 64-bit aarch64 asm 3.20 minutes 6.78 M
3.5 GHz Apple MBA-15 M2, MacOS, LPDDR5-6400 100 million primes 64-bit aarch64 asm 5.13 minutes 5.6 M
3.5 GHz Apple MBP-13 M2, Linux, LPDDR5-6400 100 million primes 64-bit aarch64 asm 5.17 minutes 5.5 M
5.0 GHz Ryzen 7 AI 350, Linux,
dual-channel 32GB LPDDR5X-7500
100 million primes 64-bit x86_64 asm
AVX SIMD 50div
7.90 minutes 2.5 M
5.1 GHz Ryzen 7 7840U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm
AVX SIMD 50div
7.93 minutes 2.5 M
5.0 GHz Ryzen 7 AI 350, Linux,
single-channel 32GB DDR5-5600
100 million primes 64-bit x86_64 asm
50div
8.1 minutes 2.47 M
4.3 GHz Intel Core Ultra 5 125U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm
50div
9.47 minutes 2.45 M
5.1 GHz Ryzen 7 7840U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm
50div
8.05 minutes 2.4 M
5.1 GHz Ryzen 7 7840U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm 8.25 minutes 2.4 M
4.7 GHz Ryzen 7 6850U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm 9.0 minutes 2.4 M
4.7 GHz Intel Core i7 1165G7, Linux, DDR4-3200 100 million primes 64-bit x86_64 asm 9.0 minutes 2.4 M
4.4 GHz Intel Core i5 1240P, Linux, LPDDR4-4267 100 million primes 64-bit x86_64 asm 9.3 minutes 2.4 M
4.5 GHz Ryzen 5 6650U, Linux, LPDDR5-6400 100 million primes 64-bit x86_64 asm 9.42 minutes 2.4 M
4.2 GHz Intel Core i5 1135G7, Linux, LPDDR4X-4266 100 million primes 64-bit x86_64 asm 9.8 minutes 2.4 M
4.2 GHz Ryzen 5 5650U, Linux, LPDDR4-4267 100 million primes 64-bit x86_64 asm 9.83 minutes 2.4 M
3.1 GHz Intel Core i5-4278U, Linux, DDR3L-1600 100 million primes 64-bit x86_64 asm
AVX-50div
19.8 minutes 1.6 M
3.1 GHz Intel Core i5-4278U, Linux, DDR3L-1600 100 million primes 64-bit i386 asm
28div
19.8 minutes 1.6 M
2.8 GHz Snapdragon 888, Android, Samsung S21 FE 100 million primes 64-bit aarch64 asm 22.7 minutes 1.6 M
2.2 GHz Snapdragon 750G, Android, Samsung A52 5G 100 million primes 64-bit aarch64 asm 29.3 minutes 1.6 M
2.8 GHz Snapdragon 8 Gen 1, Android, Samsung S23 FE 100 million primes 64-bit aarch64 asm 29.5 minutes 1.6 M
2.4 GHz AWS Xeon E5-2676, Linux 100 million primes 64-bit x86_64 asm 32.3 minutes 1.3 M
1.8 GHz Raspberry Pi 4, Linux, LPDDR4-3200 100 million primes 64-bit aarch64 asm 36.1 minutes 1.5 M
1.8 GHz Raspberry Pi 4, Linux, LPDDR4-3200 100 million primes 32-bit aarch32 asm 39.0 minutes 1.4 M
1.5 GHz Pine64 Star64 RISC-V StarFive JH-7110, Linux, LPDDR4-1867 100 million primes 64-bit riscv64 asm 126 minutes 0.53 M
2.8 GHz Intel Celeron circa 2008 105 million primes 32-bit C 80 minutes 0.47 M

  • MBA means Macbook Air.
  • MBP means Macbook Pro.
  • Linux on Apple Silicon was Fedora Asahi Linux.
  • The Core i5-4278U is an Intel Macbook Pro, tested while running MacOS.
  • 4div (aarch64) performs a sequence of 4 divisions to parallelize modulo operations using speculative execution and to minimize loop branch penalties.
  • 28div (aarch64) performs a sequence of 28 divisions to parallelize modulo operations using speculative execution and to minimize loop branch penalties.
  • 50div (x86_64) performs a sequence of 50 divisions to parallelize divisions using speculative execution and to minimize loop branch penalties.

The last column represents the efficiency of the CPU in generating primes, measured in primes per minute per GHz, with the Apple M1 trumping every other CPU.

Looking at the efficiency, the Apple M2 running the 4div variant of my code matched the Apple M3 (not Pro) also running 4div.

These beat all the recent x86 CPUs which were running non-parallelized divisions by a factor of 3.3.

In general, the efficiency of my parallelized-division is_prime routines was substantially better than doing one division per loop.

All recent x86 processors, both Intel and AMD, 2021, 2022 and 2023 models, curiously computed 2.4 million primes per minute per gigahertz with non-parallelized division.

The Raspberry Pi 4 model B was 3X more efficient than the StarFive RISC-V chip.

The Snapdragon 8 Gen 1, Snapdragon 888 and Snapdragon 750G scores were obtained by installing Termux on my Samsung S23 FE, Samsung S21 FE and A52 5G phones. Termux provides a Linux-like terminal environment, which allows installation of the Clang compiler and numerous other packages.

aarch64 implementation variants

Two basic ways of doing the time-consuming divisions:

  • I use unsigned division followed by multiply-subtract (UDIV + MSUB) instructions, one per loop.
  • Parallelizing a sequence of UDIV+MSUBs using speculative execution, 4 or 28 per loop.

  1. 100 million primes on M2 without branch prediction parallelization takes 5.3 minutes.
  2. 100 million primes on M2 with branch prediction parallelization takes 4.0 minutes.

The latter was thus 1.3X faster.

x86_64 implementation variants

I've implemented many variants to the x86_64 code in pursuit of better performance.

Varying where the prime counters on x86_64 are located and how they're used:

  • Putting counters in main registers, increasing counters, using compare-branch.
  • Putting counters in main registers, decreasing counters, using compare-branch.
  • Putting counters in main registers, stuffing 8 counters into each register, rotating to access the counters (ROR), increasing counters, compare-branch.
  • Putting counters in AVX registers, maintaining 31 7-bit counters, decreasing counters.
  • Putting counters in main registers, increasing counters, using conditional-move (very slow).

Two basic ways of doing the time-consuming divisions:

  • Unsigned integer DIV instructions, one per loop.
  • Parallelizing a sequence of unsigned integer DIV instructions using speculative execution, up to 57 per loop.

Parallelizing improves the performance of both 64:32 DIV and 128:64 DIV, for instance I did the experiment of using 128:64 DIV alone on the Intel Core i5-4278U:

  1. 100 million primes without speculative execution-based parallelization takes 60.6 minutes.
  2. 100 million primes with speculative execution-based parallelization takes 46.9 minutes.

Two types of division:

Finally on x86_64, for divisions where the dividends are 32-bit I use the 64:32-bit DIV instruction because it is 3 times as fast as the 128:64-bit DIV.

Results for a Core i5:

CPU frequency CPU name and RAM type Total primes generated Data size Implementation Duration Efficiency*
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm
AVX-50div
19.8 minutes 1.63 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm
CMP 50div
20.3 minutes 1.59 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm
ROR2 50div
20.3 minutes 1.59 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm
CMP 50div
20.5 minutes 1.58 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm 7div 21.7 minutes 1.5 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm 3div 23.7 minutes 1.4 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm CMP 27.9 minutes 1.2 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm ROR 27.9 minutes 1.2 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm ROR2 28.0 minutes 1.2 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm AVX 29.2 minutes 1.1 M
3.1 GHz Intel Core i5-4278U, DDR3L-1600 100 million primes 64-bit x86_64 asm CMOV 55.5 minutes 0.58 M

What can be learned from this is, it is the slowness of the divisions that has the most negative impact on performance.

The maintainence of the counters is secondary but impactful, if abnormally slow as when implemented with condition move (CMOV) instructions.

i386 implementation variants

There are two, based on the implementation of is_prime:

  1. Compare-branch with only one division per loop.
  2. Compare-branch with 28 divisions per loop.

I found that 28 was the sweet spot on an i5-4278U, performing better than 16, 24 or 32 divisions per loop.

203.3 million 32-bit primes

There are 203280221 primes that can be expressed in 32 bits or less, beginning with 2. Generating all of them on a 32-bit system running my 32-bit prime sieve gives the following results:

CPU, RAM Total primes generated Data size Implementation Duration Efficiency*
4.2 GHz AMD Ryzen 5 5650U, 16GB LPDDR4-4267 203.3 million primes 32-bit i386 asm 26.5 minutes 1.8 M
3.1 GHz Intel Core i5-4278U, 8GB DDR3L-1600 203.3 million primes 32-bit i386 asm
28div
55.6 minutes 1.2 M
1.8 GHz Raspberry Pi 4b, 8GB LPDDR4-3200 203.3 million primes 32-bit aarch32 asm 105 minutes 1.1 M
3.1 GHz Intel Core i5-4278U, 8GB DDR3L-1600 203.3 million primes 32-bit i386 asm 89 minutes 0.74 M

Notes:

  • Efficiency is measured in primes/minute/GHz.
  • The i5-4278U was running Raspberry Pi OS for x86, which uses 32-bit userland and a 64-bit kernel.

1 billion primes

Finding a billion primes on a 64-bit system requires allocating 7.63 GB of RAM to hold 1,000,000,000 64-bit prime numbers. If your computer has only 8GB of RAM, you may need to shut down the windowing system (e.g. X-Windows) to run this at maximum speed without paging.

CPU and RAM type Total primes generated Data size Implementation Duration Efficiency*
3.2 GHz Apple MBA-13 M1, Asahi Linux, 8GB LPDDR4X-4266 1 billion primes 64-bit aarch64 asm
28div
121.3 minutes 2.58 M
4.06 GHz Apple MBP-14 M3, MacOS, 8GB LPDDR5-6400 1 billion primes 64-bit aarch64 asm
28div
96.5 minutes 2.55 M
4.464 GHz Apple MBA-13 M5, MacOS, 24GB LPDDR5X-8533 1 billion primes 64-bit aarch64 asm
28div
89.8 minutes 2.49 M
3.48 GHz Apple MBA-13 M2, MacOS
16GB LPDDR5-6400
1 billion primes 64-bit aarch64 asm 112 minutes 2.55 M
3.2 GHz Apple MBA-13 M1, MacOS
8GB LPDDR4X-4266
1 billion primes 64-bit aarch64 asm 125.9 minutes 2.48 M
4.41 GHz Apple Mac Mini M4, MacOS, 16GB LPDDR5-7500 1 billion primes 64-bit aarch64 asm
28div
91.3 minutes 2.48 M
4.608 GHz Apple MBP-14 M5, MacOS, 16GB LPDDR5X-8533 1 billion primes 64-bit aarch64 asm
28div
88.0 minutes 2.47 M
4.06 GHz Apple MBP-14 M3, MacOS, 8GB LPDDR5-6400 1 billion primes 64-bit aarch64 asm
4div
100 minutes 2.46 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 48GB LPDDR5-8533 1 billion primes 64-bit aarch64 asm
28div
90.75 minutes 2.44 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 24GB LPDDR5-8533 1 billion primes 64-bit aarch64 asm
28div
91.15 minutes 2.43 M
4.41 GHz Apple MBA-13 M4, MacOS, 24GB LPDDR5-7500 1 billion primes 64-bit aarch64 asm
28div
96.67 minutes 2.32 M
3.48 GHz Apple MBA M2, MacOS
8GB LPDDR5-6400
1 billion primes 64-bit aarch64 asm 134.0 minutes 2.14 M
3.48 GHz Apple MBP M2, Linux
8GB LPDDR5-6400
1 billion primes 64-bit aarch64 asm 137.0 minutes 2.10 M
5.0 GHz Ryzen 7 AI 350, Linux,
dual-channel 32GB LPDDR5X-7500
1 billion primes 64-bit x86_64 asm
AVX SIMD 50div
259.3 minutes 0.77 M
5.0 GHz Ryzen 7 AI 350, Linux,
single-channel 32GB DDR5-5600
1 billion primes 64-bit x86_64 asm
AVX SIMD 50div
262.3 minutes 0.76 M
5.0 GHz Ryzen 7 AI 350, Linux,
single-channel 32GB DDR5-5600
1 billion primes 64-bit x86_64 asm
50div
262.3 minutes 0.76 M
5.1 GHz AMD Ryzen 7 7840U, Linux
32GB LPDDR5-6400
1 billion primes 64-bit x86_64 asm 259.1 minutes 0.76 M
4.2 GHz AMD Ryzen 5 5650U, Linux
16GB LPDDR4-4267
1 billion primes 64-bit x86_64 asm 313.5 minutes 0.76 M
4.4 GHz Intel Core i5 1240P, Linux
16GB LPDDR4-4267
1 billion primes 64-bit x86_64 asm 418.7 minutes 0.54 M
1.8 GHz Raspberry Pi 4b, Linux
8GB LPDDR4-3200
1 billion primes 64-bit aarch64 asm 1117 minutes 0.50 M
1.5 GHz StarFive JH-7110 RISC-V, Linux, 8GB LPDDR4-1867 1 billion primes 64-bit riscv64 asm 1845 minutes 0.36 M

Notes:

  • Efficiency is measured in primes/minute/GHz.
  • The RISC-V board was the Pine64 Star64.

The last column represents the efficiency of the CPU in generating primes, with the Apple M1 still beating the nearest contender by a factor of 3.

One conclusion one could reach is that RAM speed matters when generating 1 billion primes.

  • The Apple Silicon processors have tightly coupled RAM that communicates faster than the separate chips you see in other processors.
  • At the other extreme, the StarFive JH-7110 is coupled with slow RAM, which hampers its ability to load divisors with sufficient speed.

It is possible that the Apple Silicon CPUs have built-in divider circuits that are 3X faster than microcode-based division.

All systems were running Linux except for the Macbook Airs and the Intel Macbook Pro, which were running MacOS. The M2 Air might be slower than M1 because the M1 Air is known to have a better heat spreader on the CPU and therefore better cooling.

In the presense of super-fast RAM like in the Macs or the Ryzens, CPU speed also matters, as evidenced by the Ryzen 5 and 7 having about the same efficiency scores and differing only in CPU speed.

An interesting observation is the similarity of the efficiency scores between the Raspberry Pi 4b and the Core i5 1240P, which one might naively assume ought to be very different.

If the CPUs are inherently equally inefficient but have different clock speeds, the ratio between their clock speeds should roughly correspond to the inverse of the ratio of their run times:

Core i5-1240P Raspberry Pi 4b Ratio
Clock speed 4.4 GHz 1.8 GHz 2.5
Run time 419 minutes 1120 minutes 1/2.7

2 billion primes

Finding two billion primes on a 64-bit system requires allocating 15259 MB of RAM to hold 2,000,000,000 64-bit prime numbers.

Device Total primes generated Data size Implementation Duration Efficiency*
4.464 GHz Apple MBA-13 M5, MacOS, 24GB LPDDR5X-8533 2 billion primes 64-bit aarch64 asm
28div
247.95 minutes 1.81 M
4.41 GHz Apple Mac Mini M4, MacOS, 16GB LPDDR5-7500 2 billion primes 64-bit aarch64 asm
28div
251.9 minutes 1.80 M
4.608 GHz Apple MBP-14 M5, MacOS, 16GB LPDDR5X-8533 2 billion primes 64-bit aarch64 asm
28div
242.6 minutes 1.79 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 24GB LPDDR5-8533 2 billion primes 64-bit aarch64 asm
28div
249.2 minutes 1.78 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 48GB LPDDR5-8533 2 billion primes 64-bit aarch64 asm
28div
253.3 minutes 1.75 M
4.41 GHz Apple MBA-13 M4, MacOS, 24GB LPDDR5-7500 2 billion primes 64-bit aarch64 asm
28div
271.0 minutes 1.67 M
5.0 GHz Ryzen 7 AI 350, Linux,
single-channel 32GB DDR5-5600
2 billion primes 64-bit x86_64 asm
AVX SIMD 50div
719.7 minutes 0.556 M
5.0 GHz Ryzen 7 AI 350, Linux,
dual-channel 32GB LPDDR5X-7500
2 billion primes 64-bit x86_64 asm
AVX SIMD 50div
722.2 minutes 0.554 M
5.1 GHz AMD Ryzen 7 7840U, Linux, 32GB LPDDR5-6400 2 billion primes 64-bit x86_64 asm 719.9 minutes (12 hours) 0.545 M

Notes:

  1. The Ryzen 7 350's performance with single-channel DDR5-5600 was roughly the same as with dual-channel LPDDR5X-7500, suggesting the division operations are a more important determinant of performance than the speed of cache refilling from RAM.
  2. The above 7840U result used my original algorithm
  3. which did not use speculative execution to parallelize division. Not that it mattered much.

3 billion primes

Finding three billion primes on a 64-bit system requires allocating 22888 MB of RAM to hold 3,000,000,000 64-bit prime numbers.

Device Total primes generated Data size Implementation Duration Efficiency*

4.464 GHz Apple MBA-13 M5, MacOS, 24GB LPDDR5X-8533 3 billion primes 64-bit aarch64 asm
28div
449 minutes 1.50 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 24GB LPDDR5-8533 3 billion primes 64-bit aarch64 asm
28div
454 minutes 1.47 M
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 48GB LPDDR5-8533 3 billion primes 64-bit aarch64 asm
28div
461 minutes 1.44 M
4.41 GHz Apple MBA-13 M4, MacOS, 24GB LPDDR5-7500 3 billion primes 64-bit aarch64 asm
28div
489 minutes 1.39 M
5.0 GHz Ryzen 7 AI 350, Linux,
dual-channel 32GB LPDDR5X-7500
3 billion primes 64-bit x86_64 asm
AVX SIMD 50div
1311 minutes 0.46 M

4 billion primes

Finding three billion primes on a 64-bit system requires allocating 30518 MB of RAM to hold 4,000,000,000 64-bit prime numbers.

Device Total primes generated Data size Implementation Duration Efficiency*
4.51 GHz Apple Mac Mini M4 Pro, MacOS, 48GB LPDDR5-8533 4 billion primes 64-bit aarch64 asm
28div
702 minutes 1.26 M
5.0 GHz Ryzen 7 AI 350, Linux,
dual-channel 32GB LPDDR5X-7500
4 billion primes 64-bit x86_64 asm
AVX SIMD 50div
2003 minutes 0.34 M

  • Efficiency is measured in primes/minute/GHz.

CPU speeds

To verify the expected CPU speed of the Apple Silicon computers, one has to run the utility powermetrics which is a part of MacOS. This displays core speeds in real time. The prime64 program always migrates to a performance core and runs at the highest speed.

For x86 the speed that I give for the core that's running primes64/32 is that of Turbo Boost, because the most demanding process will typically run using that mode, if not immediately then within a second or two.

Reading the live CPU speed

On Linux, a somewhat more precise approach could be possible by continuously reading the current core speed from /sys/devices/system/cpu/cpuNUMBER/cpufreq/scaling_cur_freq for whichever core the program is running on, summing those to determine the total area under a hypothetical core speed over time graph and then at the end, dividing by the total area to get an average CPU speed. This is assuming you know which CPU the process is running on, which may change over time, but it can be obtained by calling sched_getcpu().

However I wrote the core routines in primes64 and primes32 in assembly. The main() code jumps into the core assembly routines and the programs stays there until the test is done. Calling any C function from assembly is awkward and time-consuming, so I limit calls to library functions to only printing.

Analysis

The goal of my program is to find all prime numbers up to a reasonable limit, and not just Merseinne primes or some other subset.

My algorithm has several advantages which explain its speed but it is certainly slower than Eratosthenes.

  1. By using counters to identify multiples of the small prime numbers, it avoids testing at least 87% of candidate numbers for primeness, depending on the architecture.
    • It avoids divisions for numbers that are already covered by the prime number counters.
    • The more counters it uses, the greater the percentage of candidate numbers that it can avoid checking for primality.
  2. The bookkeeping data that it uses to avoid checking numbers is entirely in the fastest memory available: CPU registers. I've tried both main registers and vector registers. The latter are usually only slightly faster.
  3. The program's memory use is proportional to the number of primes found, not the number of candidate numbers as you see with the Sieve of Eratosthenes.
  4. It minimizes divisions by only checking numbers up to 1+sqrt(number).
  5. The latest incarnations use speculative execution to perform parallel divisions on a single core, even on x86. Simpler CPUs like those in Raspberry pi's or the RISC-V board cannot do speculative parallel divisions.

Download

The source code:

Changes

  • 1.5 reinstates the original C code for use on CPUs other than x86/i386/aarch64/aarch32/riscv64.
  • 1.4 optimizes aarch64 single-core parallel division for Linux and MacOS.
  • 1.3 adds i386 single-core parallel division for Linux.
  • 1.2 adds aarch64 single-core parallel division for Linux and MacOS.
  • 1.1 optimizes x86_64, adds single-core parallel division to AVX and ROR2.
  • 1.0 adds x86_64 single-core parallel division using speculative execution.
  • 0.13 adds support for x86_64 MacOS.
  • 0.12 adds an x64 AVX SIMD experiment.
  • 0.11 adds various experiments to speed up x64.
  • 0.10 adds improvements to i386 and aarch32.
  • 0.9 adds support for RISC-V 64.
  • 0.8 adds support for i386.
  • 0.7 adds support for both Raspberry pi's aarch32 and Apple Silicon's armv8.

Useful links

1935706368