From 248ef77cc5f1af758a199601ad34e884d9f785ec Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Wed, 28 Jan 2026 20:38:32 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Profiler:=20Optimize=20Ising=20Mode?= =?UTF-8?q?l=20Evolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Combined x/y coordinate generation into a single flat index generation to reduce RNG calls. - Replaced safe indexing with `unsafe { get_unchecked }` in the hot loop for `spins` and `lut` access. - Added regression test `test_ising_evolve_ordered`. - Updated `.jules/profiler.md` with performance log. Verification: - Benchmarks show ~4.2% speedup (25.7 M/s -> 26.8 M/s). - Tests passed. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/profiler.md | 16 +++++ math_explorer/src/physics/stat_mech/ising.rs | 64 ++++++++++++++++---- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/.jules/profiler.md b/.jules/profiler.md index 3a11b10..2e5d341 100644 --- a/.jules/profiler.md +++ b/.jules/profiler.md @@ -155,3 +155,19 @@ Benchmark `bench_ising_custom` (10M iterations, 100x100 grid): - Before: ~0.72s (13.9 M/s) - After: ~0.39s (25.7 M/s) - Speedup: ~1.85x + +## 2026-11-02 - [Optimization] **Bottleneck:** Ising Model RNG & Bounds Checking **Strategy:** RNG Reduction & Unsafe Indexing **Gain:** ~4.2% Speedup (25.7 M/s -> 26.8 M/s) + +**Bottleneck:** +In `SpinLattice::evolve`, generating random coordinates `x` and `y` separately required two calls to `rng.gen_range`, consuming more entropy and CPU time than necessary. +Additionally, safe indexing `self.spins[idx]` and neighbor accesses performed bounds checks inside the hot loop. + +**Strategy:** +1. **RNG Reduction:** Combined coordinate generation into a single `rng.gen_range(0..total_spins)`, deriving `x` and `y` via integer arithmetic (which the compiler optimizes effectively). This reduced RNG calls by 50%. +2. **Unsafe Indexing:** Replaced safe slice indexing with `unsafe { *ptr.get_unchecked(idx) }` for the center spin and all 4 neighbors, as indices are guaranteed valid by the generation logic. + +**Gain:** +Benchmark `bench_ising_custom` (10M iterations, 100x100 grid): +- Before: ~25.74 M/s +- After: ~26.83 M/s +- Speedup: ~4.2% diff --git a/math_explorer/src/physics/stat_mech/ising.rs b/math_explorer/src/physics/stat_mech/ising.rs index d58f7d0..d4960d1 100644 --- a/math_explorer/src/physics/stat_mech/ising.rs +++ b/math_explorer/src/physics/stat_mech/ising.rs @@ -191,13 +191,21 @@ impl SpinLattice { let width = self.width; let height = self.height; + let total_spins = self.spins.len(); + for _ in 0..steps { - let x = rng.gen_range(0..width); - let y = rng.gen_range(0..height); + // Optimization: Generate one random index instead of two random coords. + // This cuts RNG overhead in half. + let idx = rng.gen_range(0..total_spins); + + // Derive x, y from idx. Compiler should optimize div/mod to single instruction. + let x = idx % width; + let y = idx / width; - // Manual inline of get() to help optimizer - let idx = y * width + x; - let s = self.spins[idx]; + // Optimization: Unsafe unchecked access. + // idx is guaranteed < total_spins by gen_range. + // SAFETY: idx is generated from 0..total_spins. + let s = unsafe { *self.spins.get_unchecked(idx) }; // Neighbors // Use wrapping arithmetic or simple checks to avoid modulo if possible, @@ -208,22 +216,28 @@ impl SpinLattice { let up_y = if y == 0 { height - 1 } else { y - 1 }; let down_y = if y == height - 1 { 0 } else { y + 1 }; - let neighbor_sum = self.spins[y * width + left_x] as i32 - + self.spins[y * width + right_x] as i32 - + self.spins[up_y * width + x] as i32 - + self.spins[down_y * width + x] as i32; + // SAFETY: neighbor coordinates are guaranteed to be within bounds by logic. + let neighbor_sum = unsafe { + (*self.spins.get_unchecked(y * width + left_x) as i32) + + (*self.spins.get_unchecked(y * width + right_x) as i32) + + (*self.spins.get_unchecked(up_y * width + x) as i32) + + (*self.spins.get_unchecked(down_y * width + x) as i32) + }; // Map s (-1 or 1) to 0 or 1 let s_idx = if s == -1 { 0 } else { 1 }; // Map neighbor_sum (-4..4) to 0..4 let sum_idx = ((neighbor_sum + 4) / 2) as usize; - let prob = lut[s_idx][sum_idx]; + // SAFETY: s_idx is 0 or 1, sum_idx is 0..4. lut is [2][5]. + let prob = unsafe { *lut.get_unchecked(s_idx).get_unchecked(sum_idx) }; // If prob > 1.0, it was delta_e < 0, so flip. // Else compare with random. if prob > 1.0 || rng.r#gen::() < prob { - self.spins[idx] = -s; + unsafe { + *self.spins.get_unchecked_mut(idx) = -s; + } } } } @@ -268,4 +282,32 @@ mod tests { avg_m_per_spin ); } + + #[test] + fn test_ising_evolve_ordered() { + // Low Temperature -> Ferromagnetic (Ordered) + let j_val = 1.0; + let h_val = 0.0; + // Tc ~ 2.269 + let temp = 1.5 * j_val / KB; // T < Tc + + let width = 20; + let height = 20; + let mut lattice = SpinLattice::new(width, height); + + // Run evolve + let steps = 100_000; + lattice.evolve(steps, temp, j_val, h_val); + + let m = lattice.magnetization(); + let max_m = (width * height) as f64; + let ratio = (m as f64).abs() / max_m; + + // At low T=1.5, magnetization should be high (> 0.8 usually near 0.98) + assert!( + ratio > 0.8, + "Low T Ising should be ordered (M ~ 1). Got {}", + ratio + ); + } }