Skip to content

Instantly share code, notes, and snippets.

@hpcdisrespecter
Created April 2, 2025 13:56
Show Gist options
  • Save hpcdisrespecter/b3dcb9e3f228a08c8a5177e51aaeda5d to your computer and use it in GitHub Desktop.
Save hpcdisrespecter/b3dcb9e3f228a08c8a5177e51aaeda5d to your computer and use it in GitHub Desktop.
#!/usr/env/bin python
# -*- coding: utf-8 -*-
'''
Description: This code demonstrates the following:
- Pythonic loops are slow
- List comprehensions are faster
- vectorized operations like numpy are faster than list comprehensions
- parallel computing is faster than vectorized operations for large datasets
- Just-In-Time (JIT) compilation can speed up Python code
Task:
- Create a list of n integers between 0 and 100
- Filter out the even numbers
- Square each odd number
- Compute the sum of the squared numbers
Approaches:
- Loop-based approach
- List comprehension approach
- NumPy approach
- JIT approach
- Multiprocessing approach
- Parallel joblib + JIT approach
'''
'''
Region: import libraries
'''
import os
import time
import random
import math
import numpy as np
import itertools
import multiprocessing as mp
from multiprocessing import Pool
import numba
from numba import jit
from joblib import Parallel, delayed
'''
Region: function definitions
'''
#Generate a list of n integers between 0 and 100
@jit
def generate_random_integers(n):
random.seed(42)
assert n > 0
# np broadcasting is faster than list comprehension
return np.random.randint(0, 100, n)
# Loop-based approach
def loop_approach(data):
total = 0
for i in data:
if i % 2 == 1:
total += i ** 2
return total
# List comprehension approach
def list_comprehension_approach(data):
return sum([i ** 2 for i in data if i % 2 == 1])
# NumPy approach
def numpy_approach(data):
np_data = np.array(data)
return np.sum(np.square(np_data[np_data % 2 == 1]))
# multiprocessing approach; uses all available CPU cores using subprocesses
def parallel_approach(data):
num_cores = mp.cpu_count()
chunks = np.array_split(data, num_cores)
with Pool(num_cores) as pool:
results = pool.map(numpy_approach, chunks)
return sum(results)
# JIT approach -- uses OpenMP for parallelization on the CPU
@jit
def numba_approach(data):
total = 0
for i in numba.prange(len(data)):
if data[i] % 2 == 1:
total += data[i] ** 2
return total
# Parallel computing approach -- uses joblib to parallelize the computation
def parallel_computing_approach(data):
return sum(Parallel(n_jobs=-1,prefer="threads")(delayed(numba_approach)(i) for i in np.array_split(data, mp.cpu_count())))
def main():
print("Hello, Parallel Computing Example!")
print("Task: Compute the sum of the squared odd numbers")
print("---------------------------------------------------")
# use a case statement to determine the size of the dataset
good_input = False
while not good_input:
dataset_size = input("Enter the size of the dataset 10^x: ")
try:
dataset_size = int(dataset_size)
good_input = True
except ValueError:
print("Please enter a valid integer")
n = 10 ** dataset_size
print(f"Dataset size: {n}")
data = generate_random_integers(n)
# Loop-based approach
start = time.time()
loop_approach(data)
loop_time = time.time() - start
print(f"Loop-based approach time: {loop_time} seconds")
# List comprehension approach
start = time.time()
list_comprehension_approach(data)
list_comp_time = time.time() - start
print(f"List comprehension approach: {list_comp_time} seconds")
# NumPy approach
start = time.time()
numpy_approach(data)
numpy_time = time.time() - start
print(f"NumPy approach: {numpy_time} seconds")
# JIT approach
start = time.time()
numba_approach(data)
numba_time = time.time() - start
print(f"JIT approach: {numba_time} seconds")
# Multiprocessing approach
start = time.time()
parallel_approach(data)
parallel_time = time.time() - start
print(f"Multiprocessing approach: {parallel_time} seconds")
# Parallel computing approach
start = time.time()
parallel_computing_approach(data)
parallel_computing_time = time.time() - start
print(f"Parallel computing approach: {parallel_computing_time} seconds")
print("---------------------------------------------------")
print("")
print("Summary")
print("---------------------------------------------------")
# print the speedup factor for each approach compared to the loop-based approach
print(f"List comprehension speedup: {loop_time / list_comp_time}")
print(f"NumPy speedup: {loop_time / numpy_time}")
print(f"JIT speedup: {loop_time / numba_time}")
print(f"Multiprocessing speedup: {loop_time / parallel_time}")
print(f"Parallel computing speedup: {loop_time / parallel_computing_time}")
print("---------------------------------------------------")
# do we got gnuplot ?
if os.system("which gnuplot > /dev/null") == 0:
print("Generating plot...")
with open("speedup.dat", "w") as f:
f.write("Approach Speedup\n")
f.write("Loop 1\n")
f.write("List {}\n".format(loop_time / list_comp_time))
f.write("NumPy {}\n".format(loop_time / numpy_time))
f.write("JIT {}\n".format(loop_time / numba_time))
f.write("MP {}\n".format(loop_time / parallel_time))
f.write("Parallel {}\n".format(loop_time / parallel_computing_time))
# plot to the terminal as a bar chart and scale the x-axis, and remove the legend
os.system("gnuplot -e \"set terminal dumb; set style data histogram; set style fill solid; set xtics rotate by -45; unset key; plot 'speedup.dat' using 2:xtic(1)\"")
# delete the data file
os.remove("speedup.dat")
print("---------------------------------------------------")
print("Goodbye, Parallel Computing Example!")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment