primesieve

repository·master·Indexed 22 days ago

https://github.com/kimwalisch/primesieve

A high-performance command-line program and C/C++ library for quickly generating prime numbers and prime k-tuplets up to 2^64. It utilizes a segmented sieve of Eratosthenes, wheel factorization, and SIMD vectorization to optimize performance on modern CPU architectures. The project includes a CLI for counting and printing primes, a C API, and high-level C++ classes like PrimeSieve and ParallelSieve for multi-threaded execution.

Tokens
15.1K
Snippets
57
Records
72
Agent score
76%

What's inside primesieve

  1. Optimizations used in primesieve

    master

    primesieve is highly optimized for modern CPU architectures using the following techniques:

    • Bit Array: Uses a bit array with 8 flags for every 30 numbers.
    • SIMD Vectorization: Pre-sieves multiples of small primes ($\le 163$) using SIMD instructions.
    • Cache Management:
      • Uses L1 cache for small sieving primes.
      • Uses L2 cache for medium and big sieving primes.
      • Compresses sieve and wheel indexes to improve cache efficiency.
    • Branch Optimization: Sorts medium sieving primes to reduce branch misprediction rates.
    • Parallelism: Multi-threaded execution using C++11 std::async.
    • Memory Management: Uses a custom memory pool for medium and big sieving primes.
    • Loop Optimization: Employs extreme loop unrolling in the inner sieving loop.
  2. Memory usage and complexity of primesieve

    master

    When using primesieve, you can expect the following performance characteristics:

    • Time Complexity: $O(n \log \log n)$ operations.
    • Space Complexity: $O(\sqrt{n})$ memory.
    • Practical Memory Usage: In practice, memory usage is approximately $\pi(\sqrt{n}) \times 8$ bytes per thread, where $\pi(x)$ is the prime-counting function.
  3. Use SIMD (vectorization) with primesieve::iterator

    master

    For low-level optimizations, primesieve::iterator provides access to the underlying 64-bit primes array and the generate_next_primes() method. This allows you to use SIMD instructions (like AVX512 or NEON) to process batches of primes.

    Use it.generate_next_primes() to fill the internal buffer, then iterate over it.primes_ up to it.size_ using vector intrinsics.

    #include <primesieve.hpp>
    #include <immintrin.h>
    #include <iostream>
    
    int main()
    {
      primesieve::iterator it;
      it.generate_next_primes();
    
      uint64_t limit = 10000000000;
      __m512i sums = _mm512_setzero_si512();
    
      while (it.primes_[it.size_ - 1] <= limit)
      {
        // Sum 64-bit primes using AVX512
        for (std::size_t i = 0; i < it.size_; i += 8) {
          __mmask8 mask = (i + 8 < it.size_) ? 0xff : 0xff >> (i + 8 - it.size_);
          __m512i primes = _mm512_maskz_loadu_epi64(mask, (__m512i*) &it.primes_[i]);
          sums = _mm512_add_epi64(sums, primes);
        }
    
        // Generate up to 2^10 new primes
        it.generate_next_primes();
      }
    
      // Sum the 8 partial sums
      uint64_t sum = _mm512_reduce_add_epi64(sums);
    
      // Process the remaining primes (at most 2^10)
      for (std::size_t i = 0; it.primes_[i] <= limit; i++)
        sum += it.primes_[i];
    
      std::cout << "Sum of the primes <= " << limit << ": " << sum << std::endl;
    
      return 0;
    }
  4. Optimize performance with libprimesieve

    master

    To achieve maximum performance when using libprimesieve, follow these best practices:

    • Array vs. Iterator: If you need to iterate over the same set of primes multiple times, use primesieve_generate_primes() or primesieve_generate_n_primes() to store them in an array (if RAM permits) instead of using a primesieve_iterator.
    • Direction of Iteration: primesieve_next_prime() is up to 2x faster and uses half the memory of primesieve_prev_prime(). Prefer rewriting algorithms to move forward through primes.
    • Iterator Threading: The primesieve_iterator is single-threaded. To parallelize, subdivide your sieving range into chunks and process each chunk in its own thread using its own primesieve_iterator object.
    • SIMD/Vectorization: You can access the underlying 64-bit primes array within a primesieve_iterator to apply SIMD instructions (like AVX512 or NEON) for low-level optimizations.
    • Jump Speedups: Use primesieve_jump_to() with an optional stop_hint parameter. If the sieving distance is small (e.g., < sqrt(start)), providing a stop_hint can significantly speed up the process by limiting the buffer size.
    • Avoid Repeated Initialization: Functions like primesieve_count_primes() and primesieve_nth_prime() have an $O(\sqrt{start})$ initialization overhead. Do not call them repeatedly in a loop for small sieving distances; use a primesieve_iterator instead to avoid recurring overhead.
  5. How the segmented sieve (Erat) works

    master

    Erat is the core implementation of the segmented sieve of Eratosthenes. It uses a bit array with 30 numbers per byte, where each byte holds 8 specific offsets: k = { 7, 11, 13, 17, 19, 23, 29, 31 }.

    To use the Erat implementation, you typically follow these steps:

    1. Call addSievingPrime(prime) consecutively for all primes $\le \sqrt{n}$.
    2. Call sieveSegment() to sieve the next segment of the interval.
  6. How PrimeGenerator works

    master
    PrimeGenerator is a class derived from Erat that powers the primesieve::iterator. It works by generating a batch of primes and storing them in a vector. When the iterator reaches the end of the current vector, PrimeGenerator generates a new batch of primes to ensure continuous iteration.
  7. Understand the different Erat sieve implementations

    master

    The Erat class uses specialized subclasses optimized for different types of sieving primes:

    • EratSmall: Uses a hard-coded modulo 30 wheel (skipping multiples of 2, 3, and 5). Optimized for small sieving primes that have many multiples per segment.
    • EratMedium: Also uses a modulo 30 wheel. It sorts sieving primes by their wheelIndex after the sieving step to improve CPU branch prediction, providing a ~20% speedup for medium-sized sieving primes.
    • EratBig: Uses a modulo 210 wheel (skipping multiples of 2, 3, 5, and 7) and implements Tomás Oliveira's improvement. Optimized for large sieving primes that have very few multiples per segment.
  8. Parallelize prime generation with multiple iterators

    master

    The primesieve::iterator is single-threaded. To parallelize an algorithm, you must manually subdivide the sieving distance into chunks and process each chunk in a separate thread using its own primesieve::iterator object.

    Pattern for parallelization:

    1. Subdivide the total distance into equally sized chunks.
    2. Assign each chunk to a thread.
    3. Each thread creates a primesieve::iterator(start, stop) for its specific range.
    4. Combine results (e.g., using OpenMP reduction).
    #include <primesieve.hpp>
    #include <iostream>
    #include <omp.h>
    
    int main()
    {
      uint64_t sum = 0;
      uint64_t dist = 1e10;
      int threads = omp_get_max_threads();
      uint64_t thread_dist = (dist / threads) + 1;
    
      #pragma omp parallel for reduction(+: sum)
      for (int i = 0; i < threads; i++)
      {
        uint64_t start = i * thread_dist;
        uint64_t stop = std::min(start + thread_dist, dist + 1);
        primesieve::iterator it(start, stop);
        uint64_t prime = it.next_prime();
    
        // Sum primes inside [start, stop[
        for (; prime < stop; prime = it.next_prime())
          sum += prime;
      }
    
      std::cout << "Sum of the primes <= " << dist << ": " << sum << std::endl;
    
      return 0;
    }
  9. How primesieve generates primes

    master

    primesieve uses a segmented sieve of Eratosthenes combined with wheel factorization to generate prime numbers efficiently.

    Core Algorithms

    • Segmentation: Instead of sieving the entire interval $[2, n]$ at once, the interval is subdivided into equal-sized segments sieved consecutively. This reduces memory requirements from $O(n)$ to $O(\sqrt{n})$ and allows segment sizes to fit into CPU L1 or L2 caches for faster access.
    • Wheel Factorization: This technique skips multiples of small primes. primesieve uses a modulo 210 wheel, which skips multiples of 2, 3, 5, and 7.
    • Bucket Sieve: For primes $> 2^{32}$, primesieve employs Tomás Oliveira e Silva's cache-friendly bucket list algorithm. This stores sieving primes in lists of buckets associated with specific segments, allowing the use of segments smaller than $\sqrt{n}$ without losing efficiency.
  10. Build primesieve on Unix-like OSes

    master

    To build and install primesieve on Linux or macOS, navigate to the primesieve directory and execute the following commands:

    1. Configure the project with CMake.
    2. Build the project using all available CPU cores.
    3. Install the binaries and libraries to the system.
    4. Update the shared library cache.
    cmake .
    cmake --build . --parallel
    sudo cmake --install .
    sudo ldconfig
    cmake .
    cmake --build . --parallel
    sudo cmake --install .
    sudo ldconfig
  11. Install primesieve

    master

    The primesieve command-line program can be installed using your operating system's package manager. For development with libprimesieve, you may need to install libprimesieve-dev or libprimesieve-devel depending on your distribution.

    # Windows
    winget install primesieve
    
    # macOS
    brew install primesieve
    
    # Arch Linux
    sudo pacman -S primesieve
    
    # Debian/Ubuntu
    sudo apt install primesieve
    
    # Fedora
    sudo dnf install primesieve
    
    # FreeBSD
    pkg install primesieve
    
    # openSUSE
    sudo zypper install primesieve