Skip to content

Commit 12745ea

Browse files
Optimize leapfrog_integration
The optimized code achieves a **~461x speedup** (46,112% improvement) by applying **Numba JIT compilation** with the `@numba.njit(cache=True)` decorator. This transforms Python bytecode into optimized machine code, which is crucial for computationally intensive numerical algorithms like N-body simulations. **Key optimizations:** 1. **JIT Compilation (`@numba.njit`)**: The decorator compiles the function to native machine code on first execution, eliminating Python's interpreter overhead. This is particularly effective for the triple-nested loop structure computing ~350K pairwise particle interactions (visible in line profiler showing 351,486 hits for inner loop operations). 2. **Cache enabling (`cache=True`)**: Saves compiled machine code to disk, avoiding recompilation overhead on subsequent runs—beneficial when the function is called repeatedly. 3. **Precomputed constants**: `half_dt = 0.5 * dt` and `softening_sq = softening * softening` are hoisted outside loops, eliminating redundant multiplications that were previously executed ~39K times per simulation (3 velocity updates × 12,932 particles × n_steps). **Why this speeds up the code:** The original line profiler shows the innermost loop operations (computing distances, forces, and accelerations) consume ~80% of total runtime. These tight numerical loops with scalar arithmetic are ideal candidates for Numba, which: - Eliminates CPython's dynamic type checking and function call overhead - Applies LLVM optimizations like vectorization and loop unrolling - Keeps intermediate values in CPU registers instead of creating temporary Python objects **Test case performance patterns:** - **Small particle counts** (2-3 particles): 20-40x speedup, showing JIT overhead is minimal even for simple cases - **Medium systems** (20-50 particles): 200-540x speedup, where O(N²) pairwise computations dominate - **Large systems** (100 particles): 540x speedup, demonstrating scalability as the pairwise interaction count (N×(N-1)/2) grows quadratically The optimization is universally beneficial across all test scenarios—from single particles to dense 100-particle clusters—making it ideal for any N-body simulation workload regardless of system size or timestep configuration.
1 parent 3a0e418 commit 12745ea

1 file changed

Lines changed: 13 additions & 7 deletions

File tree

code_to_optimize/sample_jit_code.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from functools import partial
22

33
import jax.numpy as jnp
4+
import numba
45
import numpy as np
56
import tensorflow as tf
67
import torch
@@ -56,6 +57,7 @@ def tridiagonal_solve(a: np.ndarray, b: np.ndarray, c: np.ndarray, d: np.ndarray
5657
return x
5758

5859

60+
@numba.njit(cache=True)
5961
def leapfrog_integration(
6062
positions: np.ndarray,
6163
velocities: np.ndarray,
@@ -71,6 +73,9 @@ def leapfrog_integration(
7173

7274
G = 1.0
7375

76+
half_dt = 0.5 * dt
77+
softening_sq = softening * softening
78+
7479
for step in range(n_steps):
7580
acc.fill(0.0)
7681

@@ -80,7 +85,7 @@ def leapfrog_integration(
8085
dy = pos[j, 1] - pos[i, 1]
8186
dz = pos[j, 2] - pos[i, 2]
8287

83-
dist_sq = dx * dx + dy * dy + dz * dz + softening * softening
88+
dist_sq = dx * dx + dy * dy + dz * dz + softening_sq
8489
dist = np.sqrt(dist_sq)
8590
dist_cubed = dist_sq * dist
8691

@@ -95,19 +100,20 @@ def leapfrog_integration(
95100
acc[j, 2] -= masses[i] * force_over_dist * dz
96101

97102
for i in range(n_particles):
98-
vel[i, 0] += 0.5 * dt * acc[i, 0]
99-
vel[i, 1] += 0.5 * dt * acc[i, 1]
100-
vel[i, 2] += 0.5 * dt * acc[i, 2]
103+
vel[i, 0] += half_dt * acc[i, 0]
104+
vel[i, 1] += half_dt * acc[i, 1]
105+
vel[i, 2] += half_dt * acc[i, 2]
106+
101107

102108
for i in range(n_particles):
103109
pos[i, 0] += dt * vel[i, 0]
104110
pos[i, 1] += dt * vel[i, 1]
105111
pos[i, 2] += dt * vel[i, 2]
106112

107113
for i in range(n_particles):
108-
vel[i, 0] += 0.5 * dt * acc[i, 0]
109-
vel[i, 1] += 0.5 * dt * acc[i, 1]
110-
vel[i, 2] += 0.5 * dt * acc[i, 2]
114+
vel[i, 0] += half_dt * acc[i, 0]
115+
vel[i, 1] += half_dt * acc[i, 1]
116+
vel[i, 2] += half_dt * acc[i, 2]
111117

112118
return pos, vel
113119

0 commit comments

Comments
 (0)