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;