From 56749c60e5c1e8ed22a1c183ccfb7dd5b3fb16f4 Mon Sep 17 00:00:00 2001 From: Saurav Bhattacharya Date: Mon, 30 Mar 2026 16:51:52 -0700 Subject: [PATCH] perf: eliminate Math.sqrt from force-directed repulsion hot path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace sqrt + division with squared-distance arithmetic in both QuadTree Barnes-Hut traversal and brute-force repulsion: - force = k²/dist; f_x = dx/dist * force → f_x = dx * k²/distSq - Barnes-Hut check: size/dist < theta → size² < theta² * distSq This avoids ~V*log(V) sqrt calls per iteration for Barnes-Hut mode and ~V²/2 calls for brute-force mode. The force direction is preserved since dx/distSq gives the correct unit vector scaling. Pre-compute k² and theta² outside the per-vertex loop to avoid redundant multiplication. --- Gvisual/src/gvisual/ForceDirectedLayout.java | 16 ++++--- Gvisual/src/gvisual/QuadTree.java | 46 +++++++++++--------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/Gvisual/src/gvisual/ForceDirectedLayout.java b/Gvisual/src/gvisual/ForceDirectedLayout.java index f9be8e2..873148c 100644 --- a/Gvisual/src/gvisual/ForceDirectedLayout.java +++ b/Gvisual/src/gvisual/ForceDirectedLayout.java @@ -193,21 +193,25 @@ public ForceDirectedLayout compute() { if (n > BARNES_HUT_THRESHOLD) { // Barnes-Hut: O(V log V) approximation via quadtree QuadTree qt = QuadTree.build(pos, n); + double kSq = k * k; + double thetaSq = BH_THETA * BH_THETA; for (int i = 0; i < n; i++) { - qt.applyRepulsion(i, pos[i][0], pos[i][1], k, disp[i], BH_THETA); + qt.applyRepulsion(i, pos[i][0], pos[i][1], kSq, disp[i], thetaSq); } } else { // Brute-force: O(V^2) all-pairs (fine for small graphs) + double kSqBF = k * k; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { double dx = pos[i][0] - pos[j][0]; double dy = pos[i][1] - pos[j][1]; - double dist = Math.sqrt(dx * dx + dy * dy); - if (dist < MIN_DIST) dist = MIN_DIST; + double distSq = dx * dx + dy * dy; + if (distSq < MIN_DIST * MIN_DIST) distSq = MIN_DIST * MIN_DIST; - double force = (k * k) / dist; - double fx = (dx / dist) * force; - double fy = (dy / dist) * force; + // force = k²/dist; fx = dx/dist * force = dx * k²/distSq + double f = kSqBF / distSq; + double fx = dx * f; + double fy = dy * f; disp[i][0] += fx; disp[i][1] += fy; diff --git a/Gvisual/src/gvisual/QuadTree.java b/Gvisual/src/gvisual/QuadTree.java index 2cc09f5..ceb39b0 100644 --- a/Gvisual/src/gvisual/QuadTree.java +++ b/Gvisual/src/gvisual/QuadTree.java @@ -19,6 +19,7 @@ final class QuadTree { private static final double MIN_DIST = 0.01; + private static final double MIN_DIST_SQ = MIN_DIST * MIN_DIST; private double cx, cy; // center of mass private int mass; // number of bodies @@ -108,42 +109,45 @@ private void putInChild(int idx, double px, double py) { * Computes repulsive force on body {@code i} at (px, py) from this * quadtree node, accumulating into disp[0] (dx) and disp[1] (dy). * - * @param i index of the body (skip self) - * @param px x-position of body i - * @param py y-position of body i - * @param k optimal distance constant - * @param disp displacement array to accumulate into [dx, dy] - * @param theta Barnes-Hut opening angle (lower = more accurate) + * @param i index of the body (skip self) + * @param px x-position of body i + * @param py y-position of body i + * @param kSq pre-computed k² (optimal distance squared) + * @param disp displacement array to accumulate into [dx, dy] + * @param thetaSq pre-computed theta² for the Barnes-Hut opening angle */ void applyRepulsion(int i, double px, double py, - double k, double[] disp, double theta) { + double kSq, double[] disp, double thetaSq) { if (mass == 0) return; double dx = px - cx; double dy = py - cy; double distSq = dx * dx + dy * dy; - double dist = Math.sqrt(distSq); if (mass == 1 && bodyIndex >= 0) { if (bodyIndex == i) return; - if (dist < MIN_DIST) dist = MIN_DIST; - double force = (k * k) / dist; - disp[0] += (dx / dist) * force; - disp[1] += (dy / dist) * force; + if (distSq < MIN_DIST_SQ) distSq = MIN_DIST_SQ; + // force = k² / dist; fx = (dx/dist)*force = dx * k² / dist² + double f = kSq / distSq; + disp[0] += dx * f; + disp[1] += dy * f; return; } - if (size / dist < theta) { - if (dist < MIN_DIST) dist = MIN_DIST; - double force = (k * k) * mass / dist; - disp[0] += (dx / dist) * force; - disp[1] += (dy / dist) * force; + // Barnes-Hut check: size/dist < theta ⟺ size²/distSq < theta² + // Avoids Math.sqrt in the common "far enough" case. + double sizeSq = size * size; + if (sizeSq < thetaSq * distSq) { + if (distSq < MIN_DIST_SQ) distSq = MIN_DIST_SQ; + double f = kSq * mass / distSq; + disp[0] += dx * f; + disp[1] += dy * f; return; } - if (nw != null) nw.applyRepulsion(i, px, py, k, disp, theta); - if (ne != null) ne.applyRepulsion(i, px, py, k, disp, theta); - if (sw != null) sw.applyRepulsion(i, px, py, k, disp, theta); - if (se != null) se.applyRepulsion(i, px, py, k, disp, theta); + if (nw != null) nw.applyRepulsion(i, px, py, kSq, disp, thetaSq); + if (ne != null) ne.applyRepulsion(i, px, py, kSq, disp, thetaSq); + if (sw != null) sw.applyRepulsion(i, px, py, kSq, disp, thetaSq); + if (se != null) se.applyRepulsion(i, px, py, kSq, disp, thetaSq); } }