Stop Using Python Loops
How I Achieved a 1000x Speedup with CuPy on Google Colab.
“Python is slow.” It’s the most common complaint developers make. But the truth is, Python isn’t slow — loops are slow.
If you are working in Data Science or AI, waiting for data preprocessing is a productivity killer. Today, I benchmarked a compute-heavy trigonometric operation on 10 million data points using four different approaches: Pure Python, NumPy, Numba, and CuPy.
Get Lovnish Verma’s stories in your inbox
Join Medium for free to get updates from this writer.
The results?
The GPU-accelerated method was 1,057 times faster than standard Python. Here is the code so you can reproduce it yourself.
The Experiment Setup
- Hardware: Google Colab (Free Tier) with a T4 GPU.
- Task: Calculate
sin(x) * cos(x)for 10,000,000 elements. - Why this task? Trigonometry is “compute-bound,” forcing the processor to work hard, which makes it the perfect stress test.
Step 1: Configuration & Hardware Check
First, let’s set up the environment and ensure we have a GPU available.
import time
import math
import numpy as np
import cupy as cp
from numba import jit, prange
from tqdm import tqdm # Progress bar for the slow Python loop
# Hardware Check
gpu_count = cp.cuda.runtime.getDeviceCount()
print(f"Environment Ready.")
print(f"GPU Detected: {gpu_count}x NVIDIA T4")
# Configuration
N_SIZE = 10_000_000
print(f"Dataset Size: {N_SIZE:,} elements")Output:
Environment Ready.
GPU Detected: 1x NVIDIA T4
Dataset Size: 10,000,000 elementsRound 1: The Baseline (Pure Python)
This is the standard for loop. Because of Python's Global Interpreter Lock (GIL) and dynamic typing, this method struggles significantly. I added a progress bar (tqdm) just to visualize the pain.
# 1. Pure Python Loop
def python_heavy_math(n):
result = 0.0
# TQDM adds a progress bar so we can see the slow execution speed
for x in tqdm(range(n), desc="Processing Python Loop"):
result += math.sin(x) * math.cos(x)
return result
print(f"Running Pure Python implementation on {N_SIZE:,} items...")
t0 = time.perf_counter()
python_heavy_math(N_SIZE)
t1 = time.perf_counter()
time_python = t1 - t0
print(f"\nPython Execution Time: {time_python:.4f} seconds")Output:
Running Pure Python implementation on 10,000,000 items...
Processing Python Loop: 100%|██████████| 10000000/10000000 [00:03<00:00]
Python Execution Time: 3.1683 secondsVerdict: 3.17 seconds. This is our baseline.
Round 2: NumPy (CPU Vectorization)
Next, we switch to NumPy. By vectorizing the operation, we push the loop execution down to optimized C code. This is the industry standard for a reason.
# 2. NumPy Implementation
print(f"Running NumPy implementation...")
# Create Data
data_np = np.arange(N_SIZE, dtype=np.float32)
t0 = time.perf_counter()
# Vectorized Operation
np.sum(np.sin(data_np) * np.cos(data_np))
t1 = time.perf_counter()
time_numpy = t1 - t0
print(f"NumPy Execution Time: {time_numpy:.4f} seconds")Output:
Running NumPy implementation...
NumPy Execution Time: 0.2251 secondsVerdict: 0.22 seconds. That is 14x faster than the Python loop. Good, but we can go faster.
Round 3: Numba (JIT Compilation)
Numba is a Just-In-Time (JIT) compiler. With a simple @jit decorator, it translates Python functions into optimized machine code that runs in parallel on the CPU.
# 3. Numba Implementation
@jit(nopython=True, parallel=True)
def numba_heavy_math(arr):
res = 0.0
for i in prange(len(arr)):
res += np.sin(arr[i]) * np.cos(arr[i])
return res
print(f"Running Numba implementation...")
# Warmup (Compilation)
numba_heavy_math(data_np)
# Benchmark
t0 = time.perf_counter()
numba_heavy_math(data_np)
t1 = time.perf_counter()
time_numba = t1 - t0
print(f"Numba Execution Time: {time_numba:.4f} seconds")Output:
Running Numba implementation...
Numba Execution Time: 0.1056 secondsVerdict: 0.10 seconds. Twice as fast as NumPy, just by adding a decorator!
Round 4: CuPy (The Nuclear Option)
Finally, we use CuPy, which is essentially NumPy for the GPU. We transfer the data to the video memory (VRAM) and calculate all 10 million points in parallel using thousands of CUDA cores.
# 4. CuPy Implementation
print(f"Running CuPy implementation...")
# Transfer data to GPU
data_cp = cp.arange(N_SIZE, dtype=cp.float32)
# Warmup
cp.sum(cp.sin(data_cp) * cp.cos(data_cp))
cp.cuda.Stream.null.synchronize()
# Benchmark
t0 = time.perf_counter()
cp.sum(cp.sin(data_cp) * cp.cos(data_cp))
cp.cuda.Stream.null.synchronize()
t1 = time.perf_counter()
time_cupy = t1 - t0
print(f"CuPy Execution Time: {time_cupy:.4f} seconds")Output:
Running CuPy implementation...
CuPy Execution Time: 0.0030 secondsVerdict: 0.003 seconds. This is instantaneous.
Final Results & Summary
Here is the final breakdown of the benchmark:
----------------------------------------
BENCHMARK RESULTS (Lower is Better)
----------------------------------------
1. Pure Python: 3.1683 s
2. NumPy (CPU): 0.2251 s
3. Numba (CPU): 0.1056 s
4. CuPy (GPU): 0.0030 s
----------------------------------------
SPEEDUP FACTOR:
CuPy is 75.1x faster than NumPy
CuPy is 1057.5x faster than PythonKey Takeaways
- CuPy is 75x faster than NumPy: If you have a GPU, use it.
- CuPy is 1000x faster than Python loops: Never write raw loops for math operations.
- Numba is a great CPU alternative: If you don’t have a GPU, Numba still gave us a 2x speedup over NumPy.
The math didn’t change, only the library did. Stop using lists for heavy calculations!
For more tutorials like this, subscribe to my YouTube channel: Lovnish Verma










