From 8229e3a50f4e62ec753168afcef6b7d691019061 Mon Sep 17 00:00:00 2001 From: Saurav Bhattacharya Date: Fri, 3 Apr 2026 03:39:45 -0700 Subject: [PATCH] perf: use array-indexed tracking in hittingTimesFrom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace HashSet + Map per-simulation allocations with int[]-based tracking in RandomWalkAnalyzer.hittingTimesFrom(): - Vertex-to-index mapping eliminates autoboxing and Map.get() in hot loop - Generation counter for visited tracking: O(1) reset per simulation instead of allocating a new HashSet(V) × 10,000 simulations - Pre-built int[][] adjacency for cache-friendly neighbor access - long[]/int[] accumulators instead of LinkedHashMap For a graph with V=100 vertices, this eliminates ~10,000 HashSet allocations of 100 elements each, plus ~20,000 Map.get()/put() calls per simulation in the inner loop. --- Gvisual/src/gvisual/RandomWalkAnalyzer.java | 98 +++++++++++++-------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/Gvisual/src/gvisual/RandomWalkAnalyzer.java b/Gvisual/src/gvisual/RandomWalkAnalyzer.java index 6496ebd..c532037 100644 --- a/Gvisual/src/gvisual/RandomWalkAnalyzer.java +++ b/Gvisual/src/gvisual/RandomWalkAnalyzer.java @@ -67,63 +67,89 @@ public double hittingTime(Graph graph, V source, V target) { * every unvisited vertex along the way. For a graph with V vertices * and 10,000 simulations, this reduces total walks from V × 10,000 * to just 10,000 — a V× speedup.

+ * + *

Array-indexed tracking: Uses integer-indexed arrays instead + * of {@code HashSet} for visited tracking and {@code Map} + * for accumulators. This eliminates per-simulation HashSet allocation + * (previously O(V) per sim × 10,000 sims = significant GC pressure), + * avoids autoboxing overhead, and provides cache-friendly sequential + * access. The visited array is reset via a generation counter rather + * than {@code Arrays.fill}, turning the O(V) per-sim reset into O(1).

*/ public Map hittingTimesFrom(Graph graph, V source) { validateGraph(graph); validateNode(graph, source, "source"); - // Initialize accumulators - Map totalSteps = new LinkedHashMap<>(); - Map reachedCount = new LinkedHashMap<>(); - for (V v : graph.getVertices()) { - totalSteps.put(v, 0L); - reachedCount.put(v, 0); + int n = graph.getVertexCount(); + List vertexList = new ArrayList<>(graph.getVertices()); + Map vertexIndex = new HashMap<>(n * 2); + for (int i = 0; i < n; i++) { + vertexIndex.put(vertexList.get(i), i); } - // Self-hitting time is always 0 - totalSteps.put(source, 0L); - reachedCount.put(source, defaultSimulations); + int sourceIdx = vertexIndex.get(source); - int maxSteps = graph.getVertexCount() * graph.getVertexCount() * 10; + // Array-based accumulators (no boxing, no Map lookups in hot loop) + long[] totalSteps = new long[n]; + int[] reachedCount = new int[n]; + reachedCount[sourceIdx] = defaultSimulations; + + int maxSteps = n * n * 10; - // Cache neighbor lists for fast random selection - Map> neighborCache = new HashMap<>(); - for (V v : graph.getVertices()) { + // Build adjacency as int[][] for cache-friendly, boxing-free traversal + int[][] adj = new int[n][]; + for (int i = 0; i < n; i++) { + V v = vertexList.get(i); Collection nbrs = graph.getNeighbors(v); - neighborCache.put(v, nbrs != null ? new ArrayList<>(nbrs) : Collections.emptyList()); + if (nbrs == null || nbrs.isEmpty()) { + adj[i] = new int[0]; + } else { + int[] neighbors = new int[nbrs.size()]; + int j = 0; + for (V nb : nbrs) { + Integer idx = vertexIndex.get(nb); + if (idx != null) neighbors[j++] = idx; + } + adj[i] = (j == neighbors.length) ? neighbors : java.util.Arrays.copyOf(neighbors, j); + } } - int targetCount = graph.getVertexCount() - 1; // exclude source + // Generation-based visited tracking: instead of allocating a new + // HashSet or calling Arrays.fill(visited, false) each simulation, + // we increment a generation counter. A vertex is "visited" when + // visitedGen[v] == currentGen. Reset is O(1) per simulation. + int[] visitedGen = new int[n]; + int currentGen = 0; for (int sim = 0; sim < defaultSimulations; sim++) { - // Track which vertices we haven't visited yet in this walk - Set remaining = new HashSet<>(graph.getVertices()); - remaining.remove(source); - - V current = source; - for (int step = 1; step <= maxSteps && !remaining.isEmpty(); step++) { - List nbrs = neighborCache.get(current); - if (nbrs == null || nbrs.isEmpty()) break; - current = nbrs.get(rng.nextInt(nbrs.size())); - - if (remaining.remove(current)) { - // First visit to this vertex in this walk - totalSteps.put(current, totalSteps.get(current) + step); - reachedCount.put(current, reachedCount.get(current) + 1); + currentGen++; + visitedGen[sourceIdx] = currentGen; + int remaining = n - 1; // count of unvisited vertices + + int currentIdx = sourceIdx; + for (int step = 1; step <= maxSteps && remaining > 0; step++) { + int[] nbrs = adj[currentIdx]; + if (nbrs.length == 0) break; + currentIdx = nbrs[rng.nextInt(nbrs.length)]; + + if (visitedGen[currentIdx] != currentGen) { + visitedGen[currentIdx] = currentGen; + remaining--; + totalSteps[currentIdx] += step; + reachedCount[currentIdx]++; } } } - // Compute averages + // Build result map Map result = new LinkedHashMap<>(); - for (V v : graph.getVertices()) { - if (v.equals(source)) { - result.put(v, 0.0); + for (int i = 0; i < n; i++) { + if (i == sourceIdx) { + result.put(vertexList.get(i), 0.0); } else { - int reached = reachedCount.get(v); - result.put(v, reached == 0 + result.put(vertexList.get(i), reachedCount[i] == 0 ? Double.POSITIVE_INFINITY - : (double) totalSteps.get(v) / reached); + : (double) totalSteps[i] / reachedCount[i]); } } return result;