iph.ar
  • home
  • Misc.
  • Gaming
  • (de)Engineering
    • Using LLMs to Simulate Wild Thinking and Convergence
    • Visualizing Song Structure with Self-Similarity Matrices
    • PyHPC – Self-Guided Parallel Programming Workshop with Python (WIP)
      • 00 - How This Course Was Made
      • 01 - Multiprocessing
        • 1.1 - HPC landscape overview
        • 1.2 - Introduction to the multiprocessing module
        • 1.3 - Using Process, Queue, Pipe, and Pool
        • 1.4 - Introduction to concurrent.futures
        • 1.5 - Examples of CPU-bound tasks with performance profiling
        • 1.6 - Comparison with sequential execution
        • 1.7 - Synchronization and locking considerations
        • 1.8 - Profiling CPU-bound vs I/O-bound tasks
        • Solution: Parallel prime number calculation
        • Solution: Signal filtering comparison
      • 02 - Multithreading and GIL
      • 03 - MPI with mpi4py
      • 04 - GPU with PyCUDA/Numba
      • 05 - Parallel Libraries
    • Mapping Consonants as Percussion: A Small Experiment with Whisper and Audio Analysis
    • Semantic Self-Similarity or How I Split a Conversation into Scenes Using Language Models
    • Modeling the Noise: Building a Tinnitus Generator in Python
    • Making A Humble OpenGL Rotating Cube
    • Semantic Analysis Applied to "Sent From My Telephone" by Voice Actor
    • Longitudinal Sentiment Analysis of Personal Chat Logs
  • IT
  • home
  • Misc.
  • Gaming
  • (de)Engineering
    • Using LLMs to Simulate Wild Thinking and Convergence
    • Visualizing Song Structure with Self-Similarity Matrices
    • PyHPC – Self-Guided Parallel Programming Workshop with Python (WIP)
      • 00 - How This Course Was Made
      • 01 - Multiprocessing
        • 1.1 - HPC landscape overview
        • 1.2 - Introduction to the multiprocessing module
        • 1.3 - Using Process, Queue, Pipe, and Pool
        • 1.4 - Introduction to concurrent.futures
        • 1.5 - Examples of CPU-bound tasks with performance profiling
        • 1.6 - Comparison with sequential execution
        • 1.7 - Synchronization and locking considerations
        • 1.8 - Profiling CPU-bound vs I/O-bound tasks
        • Solution: Parallel prime number calculation
        • Solution: Signal filtering comparison
      • 02 - Multithreading and GIL
      • 03 - MPI with mpi4py
      • 04 - GPU with PyCUDA/Numba
      • 05 - Parallel Libraries
    • Mapping Consonants as Percussion: A Small Experiment with Whisper and Audio Analysis
    • Semantic Self-Similarity or How I Split a Conversation into Scenes Using Language Models
    • Modeling the Noise: Building a Tinnitus Generator in Python
    • Making A Humble OpenGL Rotating Cube
    • Semantic Analysis Applied to "Sent From My Telephone" by Voice Actor
    • Longitudinal Sentiment Analysis of Personal Chat Logs
  • IT

Solution: Parallel prime number calculation

September 2025

Exercise: Parallel prime number calculation

  • Sequential (old-fashioned) β†’ simple loop, no dependencies, uses only built-in Python.
  • Parallel version (multiprocessing) β†’ same logic, but split into chunks and run in parallel.

Sequential version

# primes_seq.py
# Sequential prime number calculation
# No pip install required (only built-in Python)

import sys

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def primes_up_to(n):
    return [x for x in range(2, n + 1) if is_prime(x)]

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python primes_seq.py <n>")
        sys.exit(1)

    n = int(sys.argv[1])
    primes = primes_up_to(n)
    print(f"Primes up to {n}: {primes}")

πŸ‘‰ Run it with:

python primes_seq.py 50

Parallel version

# primes_parallel.py
# Parallel prime number calculation using multiprocessing
# No pip install required (only built-in Python)

import sys
from multiprocessing import Process, Queue, cpu_count

def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def primes_in_range(start, end, q):
    result = [x for x in range(start, end) if is_prime(x)]
    q.put(result)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python primes_parallel.py <n>")
        sys.exit(1)

    n = int(sys.argv[1])
    num_procs = cpu_count()  # use available cores
    chunk_size = n // num_procs + 1

    q = Queue()
    procs = []

    # Divide the range into chunks
    for i in range(num_procs):
        start = i * chunk_size + 2
        end = min((i + 1) * chunk_size + 2, n + 1)
        p = Process(target=primes_in_range, args=(start, end, q))
        p.start()
        procs.append(p)

    # Collect results
    all_primes = []
    for _ in procs:
        all_primes.extend(q.get())

    for p in procs:
        p.join()

    all_primes.sort()
    print(f"Primes up to {n}: {all_primes}")

πŸ‘‰ Run it with:

python primes_parallel.py 50

Let’s build a benchmarking helper script that:

  • Runs the prime calculation with different values of n (10k, 100k, 1M, 10M).
  • Varies the number of processes (1, 2, 4, 8).
  • Records elapsed time for each run.
  • Displays a bar chart comparing times (requires only matplotlib, so pip install matplotlib if students don’t already have it).
# benchmark_primes.py
# Compare sequential vs parallel prime calculation
# Requires matplotlib: pip install matplotlib

import time
import matplotlib.pyplot as plt
from multiprocessing import Process, Queue

# --- Prime helpers (same as before) ---
def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True

def primes_in_range(start, end, q):
    result = [x for x in range(start, end) if is_prime(x)]
    q.put(result)

def primes_parallel(n, num_procs):
    chunk_size = n // num_procs + 1
    q = Queue()
    procs = []

    for i in range(num_procs):
        start = i * chunk_size + 2
        end = min((i + 1) * chunk_size + 2, n + 1)
        p = Process(target=primes_in_range, args=(start, end, q))
        p.start()
        procs.append(p)

    all_primes = []
    for _ in procs:
        all_primes.extend(q.get())

    for p in procs:
        p.join()

    all_primes.sort()
    return all_primes

# --- Benchmarking logic ---
def benchmark(n, num_procs):
    t0 = time.time()
    primes_parallel(n, num_procs)
    return time.time() - t0

if __name__ == "__main__":
    ns = [10_000, 100_000, 1_000_000, 10_000_000]
    proc_counts = [1, 2, 4, 8]

    results = {}

    for n in ns:
        results[n] = []
        for p in proc_counts:
            print(f"Running n={n:,} with {p} process(es)...")
            elapsed = benchmark(n, p)
            results[n].append(elapsed)

    # --- Plot results ---
    fig, ax = plt.subplots(figsize=(10, 6))

    bar_width = 0.2
    x = range(len(ns))
    for i, p in enumerate(proc_counts):
        times = [results[n][i] for n in ns]
        ax.bar([xi + i * bar_width for xi in x], times,
               width=bar_width, label=f"{p} proc(s)")

    ax.set_xticks([xi + 1.5 * bar_width for xi in x])
    ax.set_xticklabels([f"{n:,}" for n in ns])
    ax.set_ylabel("Time (s)")
    ax.set_xlabel("n (max number checked)")
    ax.set_title("Prime calculation benchmark")
    ax.legend()
    plt.tight_layout()
    plt.show()
at 100k and bellow, the plot shows the parallelization overhead

⟡ 1.8 - Profiling CPU-bound vs I/O-bound tasks Solution: Signal filtering comparison ⟢
  • © 2026 iph.ar