Skip to content

Commit 85a15d1

Browse files
committed
perf: optimization on using special maps to reduce RAM and GC pressure
1 parent b18635a commit 85a15d1

7 files changed

Lines changed: 327 additions & 72 deletions

File tree

engine/src/main/java/com/arcadedb/graph/olap/DeltaOverlay.java

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package com.arcadedb.graph.olap;
2020

2121
import com.arcadedb.database.RID;
22+
import com.arcadedb.utility.IntIntHashMap;
2223

2324
import java.util.*;
2425

@@ -58,8 +59,8 @@ class DeltaOverlay {
5859
private final Map<String, Set<Long>> deletedEdgesPerType;
5960

6061
// Per-node deleted edge counts for O(1) lookup: edgeType -> nodeId -> count
61-
private final Map<String, Map<Integer, Integer>> deletedOutEdgeCounts;
62-
private final Map<String, Map<Integer, Integer>> deletedInEdgeCounts;
62+
private final Map<String, IntIntHashMap> deletedOutEdgeCounts;
63+
private final Map<String, IntIntHashMap> deletedInEdgeCounts;
6364

6465
// Property overrides for base nodes: globalId -> (propName -> value)
6566
private final Map<Integer, Map<String, Object>> propertyOverrides;
@@ -97,8 +98,8 @@ private DeltaOverlay(final int baseNodeCount,
9798
final BitSet deletedBaseNodes, final BitSet deletedOverflowNodes,
9899
final Map<String, List<long[]>> addedEdgesPerType,
99100
final Map<String, Set<Long>> deletedEdgesPerType,
100-
final Map<String, Map<Integer, Integer>> deletedOutEdgeCounts,
101-
final Map<String, Map<Integer, Integer>> deletedInEdgeCounts,
101+
final Map<String, IntIntHashMap> deletedOutEdgeCounts,
102+
final Map<String, IntIntHashMap> deletedInEdgeCounts,
102103
final Map<Integer, Map<String, Object>> propertyOverrides,
103104
final Map<String, Map<Integer, int[]>> outNeighborIndex,
104105
final Map<String, Map<Integer, int[]>> inNeighborIndex,
@@ -204,8 +205,8 @@ DeltaOverlay merge(final TxDelta delta, final NodeIdMapping baseMapping) {
204205
final Map<String, Map<Integer, int[]>> newInIndex = buildNeighborIndex(newAddedEdges, false);
205206

206207
// Build per-node deleted edge count indexes for O(1) lookup
207-
final Map<String, Map<Integer, Integer>> newDelOutCounts = buildDeletedEdgeCounts(newDeletedEdges, true);
208-
final Map<String, Map<Integer, Integer>> newDelInCounts = buildDeletedEdgeCounts(newDeletedEdges, false);
208+
final Map<String, IntIntHashMap> newDelOutCounts = buildDeletedEdgeCounts(newDeletedEdges, true);
209+
final Map<String, IntIntHashMap> newDelInCounts = buildDeletedEdgeCounts(newDeletedEdges, false);
209210

210211
return new DeltaOverlay(baseNodeCount,
211212
Collections.unmodifiableMap(newOverflowIds),
@@ -266,20 +267,20 @@ boolean isEdgeDeleted(final String edgeType, final int srcId, final int tgtId) {
266267
* Counts the number of deleted outgoing edges from {@code nodeId} for the given edge type. O(1).
267268
*/
268269
int countDeletedOutEdges(final int nodeId, final String edgeType) {
269-
final Map<Integer, Integer> counts = deletedOutEdgeCounts.get(edgeType);
270+
final IntIntHashMap counts = deletedOutEdgeCounts.get(edgeType);
270271
if (counts == null)
271272
return 0;
272-
return counts.getOrDefault(nodeId, 0);
273+
return counts.get(nodeId, 0);
273274
}
274275

275276
/**
276277
* Counts the number of deleted incoming edges to {@code nodeId} for the given edge type. O(1).
277278
*/
278279
int countDeletedInEdges(final int nodeId, final String edgeType) {
279-
final Map<Integer, Integer> counts = deletedInEdgeCounts.get(edgeType);
280+
final IntIntHashMap counts = deletedInEdgeCounts.get(edgeType);
280281
if (counts == null)
281282
return 0;
282-
return counts.getOrDefault(nodeId, 0);
283+
return counts.get(nodeId, 0);
283284
}
284285

285286
/**
@@ -355,24 +356,25 @@ private static Map<String, Map<Integer, int[]>> buildNeighborIndex(
355356
final List<long[]> edges = entry.getValue();
356357

357358
// Pass 1: count neighbors per node
358-
final HashMap<Integer, Integer> counts = new HashMap<>();
359+
final IntIntHashMap counts = new IntIntHashMap();
359360
for (final long[] pair : edges) {
360361
final int key = (int) (outgoing ? pair[0] : pair[1]);
361-
counts.merge(key, 1, Integer::sum);
362+
counts.increment(key);
362363
}
363364

364365
// Allocate exact-size arrays
365366
final Map<Integer, int[]> perNode = new HashMap<>(counts.size());
366-
for (final var e : counts.entrySet())
367-
perNode.put(e.getKey(), new int[e.getValue()]);
367+
counts.forEach((key, count) -> perNode.put(key, new int[count]));
368368

369-
// Pass 2: fill arrays (reuse counts map as fill-position tracker)
370-
counts.replaceAll((k, v) -> 0);
369+
// Pass 2: fill arrays (use a fresh map as fill-position tracker)
370+
final IntIntHashMap positions = new IntIntHashMap(counts.size());
371371
for (final long[] pair : edges) {
372372
final int key = (int) (outgoing ? pair[0] : pair[1]);
373373
final int neighbor = (int) (outgoing ? pair[1] : pair[0]);
374374
final int[] arr = perNode.get(key);
375-
arr[counts.merge(key, 1, Integer::sum) - 1] = neighbor;
375+
final int pos = positions.get(key, 0);
376+
arr[pos] = neighbor;
377+
positions.put(key, pos + 1);
376378
}
377379

378380
result.put(entry.getKey(), perNode);
@@ -385,16 +387,16 @@ private static Map<String, Map<Integer, int[]>> buildNeighborIndex(
385387
*
386388
* @param outgoing true for outgoing counts (keyed by source), false for incoming (keyed by target)
387389
*/
388-
private static Map<String, Map<Integer, Integer>> buildDeletedEdgeCounts(
390+
private static Map<String, IntIntHashMap> buildDeletedEdgeCounts(
389391
final Map<String, Set<Long>> deletedEdges, final boolean outgoing) {
390392
if (deletedEdges.isEmpty())
391393
return Collections.emptyMap();
392-
final Map<String, Map<Integer, Integer>> result = new HashMap<>();
394+
final Map<String, IntIntHashMap> result = new HashMap<>();
393395
for (final var entry : deletedEdges.entrySet()) {
394-
final Map<Integer, Integer> counts = new HashMap<>();
396+
final IntIntHashMap counts = new IntIntHashMap();
395397
for (final long packed : entry.getValue()) {
396398
final int nodeId = outgoing ? (int) (packed >>> 32) : (int) packed;
397-
counts.merge(nodeId, 1, Integer::sum);
399+
counts.increment(nodeId);
398400
}
399401
result.put(entry.getKey(), counts);
400402
}

engine/src/main/java/com/arcadedb/query/opencypher/executor/steps/PairHashJoinOp.java

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.arcadedb.graph.Vertex;
2727
import com.arcadedb.schema.DocumentType;
2828
import com.arcadedb.schema.VertexType;
29+
import com.arcadedb.utility.LongLongHashMap;
2930

3031
import java.util.ArrayList;
3132
import java.util.HashMap;
@@ -96,7 +97,7 @@ public long execute(final GraphTraversalProvider provider, final Database db) {
9697
// BUILD: for single-hop arms, use NeighborView for direct edge iteration
9798
// instead of per-node walkArm (avoids ~6M getNeighborIds method calls).
9899
// For multi-hop arms, fall back to per-node walkArm.
99-
final HashMap<Long, Long> pairCounts = new HashMap<>();
100+
final LongLongHashMap pairCounts = new LongLongHashMap();
100101

101102
final boolean arm1SingleHop = arm1EdgeTypes.length == 1;
102103
final boolean arm2SingleHop = arm2EdgeTypes.length == 1;
@@ -145,29 +146,22 @@ public long execute(final GraphTraversalProvider provider, final Database db) {
145146
continue;
146147
for (final int ep1 : ep1Ids)
147148
for (final int ep2 : ep2Ids)
148-
pairCounts.merge(CSRCountUtils.packPair(ep1, ep2), 1L, Long::sum);
149+
pairCounts.increment(CSRCountUtils.packPair(ep1, ep2));
149150
}
150151
}
151152

152153
final NeighborView probeViewFallback = probeView != null ? probeView : provider.getNeighborView(probeDirection, probeEdgeType);
153154
long total = 0;
154155
if (probeViewFallback != null) {
155156
final int[] probeNbrs = probeViewFallback.neighbors();
156-
for (int p1 = 0; p1 < nodeCount; p1++) {
157-
for (int j = probeViewFallback.offset(p1), end = probeViewFallback.offsetEnd(p1); j < end; j++) {
158-
final Long cnt = pairCounts.get(CSRCountUtils.packPair(p1, probeNbrs[j]));
159-
if (cnt != null)
160-
total += cnt;
161-
}
162-
}
157+
for (int p1 = 0; p1 < nodeCount; p1++)
158+
for (int j = probeViewFallback.offset(p1), end = probeViewFallback.offsetEnd(p1); j < end; j++)
159+
total += pairCounts.get(CSRCountUtils.packPair(p1, probeNbrs[j]), 0);
163160
} else {
164161
for (int p1 = 0; p1 < nodeCount; p1++) {
165162
final int[] neighbors = provider.getNeighborIds(p1, probeDirection, probeEdgeType);
166-
for (final int p2 : neighbors) {
167-
final Long cnt = pairCounts.get(CSRCountUtils.packPair(p1, p2));
168-
if (cnt != null)
169-
total += cnt;
170-
}
163+
for (final int p2 : neighbors)
164+
total += pairCounts.get(CSRCountUtils.packPair(p1, p2), 0);
171165
}
172166
}
173167
return total;
@@ -273,7 +267,7 @@ private long buildAndProbeInline(final NeighborView arm1View, final NeighborView
273267
* But for simpler pair-joins where both arms are 1 hop, this avoids all per-node method calls.
274268
*/
275269
private void buildWithViews(final NeighborView arm1View, final NeighborView arm2View,
276-
final Set<Integer>[] arm2Buckets, final HashMap<Long, Long> pairCounts,
270+
final Set<Integer>[] arm2Buckets, final LongLongHashMap pairCounts,
277271
final int nodeCount, final GraphTraversalProvider provider) {
278272
final int[] arm1Nbrs = arm1View.neighbors();
279273
final int[] arm2Nbrs = arm2View.neighbors();
@@ -287,7 +281,7 @@ private void buildWithViews(final NeighborView arm1View, final NeighborView arm2
287281

288282
for (int i = a1Start; i < a1End; i++)
289283
for (int j = a2Start; j < a2End; j++)
290-
pairCounts.merge(CSRCountUtils.packPair(arm1Nbrs[i], arm2Nbrs[j]), 1L, Long::sum);
284+
pairCounts.increment(CSRCountUtils.packPair(arm1Nbrs[i], arm2Nbrs[j]));
291285
}
292286
}
293287

@@ -297,7 +291,7 @@ private void buildWithViews(final NeighborView arm1View, final NeighborView arm2
297291
* For Q2: arm1=HAS_CREATOR OUT (Comment→Person), arm2=REPLY_OF+HAS_CREATOR (Comment→Post→Person).
298292
*/
299293
private void buildWithArm1View(final NeighborView arm1View, final GraphTraversalProvider provider,
300-
final Set<Integer>[] arm2Buckets, final HashMap<Long, Long> pairCounts,
294+
final Set<Integer>[] arm2Buckets, final LongLongHashMap pairCounts,
301295
final int nodeCount) {
302296
final int[] arm1Nbrs = arm1View.neighbors();
303297

@@ -332,7 +326,7 @@ private void buildWithArm1View(final NeighborView arm1View, final GraphTraversal
332326

333327
for (int i = a1Start; i < a1End; i++)
334328
for (final int ep2 : ep2Ids)
335-
pairCounts.merge(CSRCountUtils.packPair(arm1Nbrs[i], ep2), 1L, Long::sum);
329+
pairCounts.increment(CSRCountUtils.packPair(arm1Nbrs[i], ep2));
336330
}
337331
}
338332

engine/src/main/java/com/arcadedb/query/opencypher/procedures/algo/AlgoLabelPropagation.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
import com.arcadedb.query.sql.executor.Result;
2929
import com.arcadedb.query.sql.executor.ResultInternal;
3030

31+
import com.arcadedb.utility.IntIntHashMap;
32+
3133
import java.util.ArrayList;
32-
import java.util.HashMap;
3334
import java.util.Iterator;
3435
import java.util.List;
3536
import java.util.Map;
@@ -165,19 +166,19 @@ private Stream<Result> executeWithOLTP(final Database db, final int maxIteration
165166
}
166167

167168
// Count labels of neighbors
168-
final Map<Integer, Integer> labelCount = new HashMap<>();
169+
final IntIntHashMap labelCount = new IntIntHashMap();
169170
for (final int neighborIdx : neighbors)
170-
labelCount.merge(label[neighborIdx], 1, Integer::sum);
171+
labelCount.increment(label[neighborIdx]);
171172

172173
// Find most frequent label (ties broken by smallest label)
173-
int bestLabel = label[i];
174-
int bestCount = 0;
175-
for (final Map.Entry<Integer, Integer> entry : labelCount.entrySet()) {
176-
if (entry.getValue() > bestCount || (entry.getValue() == bestCount && entry.getKey() < bestLabel)) {
177-
bestCount = entry.getValue();
178-
bestLabel = entry.getKey();
174+
final int[] best = { label[i], 0 }; // [bestLabel, bestCount]
175+
labelCount.forEach((lbl, cnt) -> {
176+
if (cnt > best[1] || (cnt == best[1] && lbl < best[0])) {
177+
best[1] = cnt;
178+
best[0] = lbl;
179179
}
180-
}
180+
});
181+
int bestLabel = best[0];
181182

182183
newLabel[i] = bestLabel;
183184
if (bestLabel != label[i])

engine/src/main/java/com/arcadedb/query/opencypher/procedures/algo/AlgoLeiden.java

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import com.arcadedb.query.sql.executor.Result;
2626
import com.arcadedb.query.sql.executor.ResultInternal;
2727

28-
import java.util.HashMap;
28+
import com.arcadedb.utility.IntIntHashMap;
29+
2930
import java.util.List;
30-
import java.util.Map;
3131
import java.util.stream.IntStream;
3232
import java.util.stream.Stream;
3333

@@ -125,30 +125,29 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
125125
final long ki = degree[i];
126126

127127
// Count weights to each neighboring community
128-
final Map<Integer, Integer> commWeights = new HashMap<>();
129-
for (final int j : adj[i]) {
130-
final int jComm = community[j];
131-
commWeights.merge(jComm, 1, Integer::sum);
132-
}
128+
final IntIntHashMap commWeights = new IntIntHashMap();
129+
for (final int j : adj[i])
130+
commWeights.increment(community[j]);
133131

134132
int bestComm = currentComm;
135-
double bestGain = 0.0;
133+
final double[] bestGain = { 0.0 };
134+
final int[] bestCommHolder = { currentComm };
136135

137-
for (final Map.Entry<Integer, Integer> entry : commWeights.entrySet()) {
138-
final int candidateComm = entry.getKey();
136+
commWeights.forEach((candidateComm, weight) -> {
139137
if (candidateComm == currentComm)
140-
continue;
138+
return;
141139

142-
final double eIc = entry.getValue();
140+
final double eIc = weight;
143141
final double dc = communityDegree[candidateComm];
144142
// Modularity gain = [e_ic / m] - γ * [k_i * d_c / (2m²)]
145143
final double gain = eIc / m - resolution * ki * dc / (2.0 * m * m);
146144

147-
if (gain > bestGain) {
148-
bestGain = gain;
149-
bestComm = candidateComm;
145+
if (gain > bestGain[0]) {
146+
bestGain[0] = gain;
147+
bestCommHolder[0] = candidateComm;
150148
}
151-
}
149+
});
150+
bestComm = bestCommHolder[0];
152151

153152
if (bestComm != currentComm) {
154153
communityDegree[currentComm] -= ki;
@@ -166,14 +165,14 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
166165
final int currentComm = community[i];
167166
final long ki = degree[i];
168167

169-
final Map<Integer, Integer> sameCommWeights = new HashMap<>();
168+
final IntIntHashMap sameCommWeights = new IntIntHashMap();
170169
for (final int j : adj[i]) {
171170
if (community[j] == currentComm)
172-
sameCommWeights.merge(community[j], 1, Integer::sum);
171+
sameCommWeights.increment(community[j]);
173172
}
174173

175174
// Try removing from current community
176-
final int internalEdges = sameCommWeights.getOrDefault(currentComm, 0);
175+
final int internalEdges = sameCommWeights.get(currentComm, 0);
177176
final double removeGain = -(internalEdges / m - resolution * ki * communityDegree[currentComm] / (2.0 * m * m));
178177

179178
if (removeGain > 0.0) {
@@ -196,7 +195,7 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
196195
}
197196

198197
// Remap communities to sequential IDs
199-
final Map<Integer, Integer> remap = new HashMap<>();
198+
final IntIntHashMap remap = new IntIntHashMap();
200199
int nextId = 0;
201200
for (int i = 0; i < n; i++) {
202201
if (!remap.containsKey(community[i]))
@@ -206,7 +205,7 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
206205
return IntStream.range(0, n).mapToObj(i -> {
207206
final ResultInternal r = new ResultInternal();
208207
r.setProperty("nodeId", graph.getRID(i));
209-
r.setProperty("community", remap.get(community[i]));
208+
r.setProperty("community", remap.get(community[i], -1));
210209
return (Result) r;
211210
});
212211
}

engine/src/main/java/com/arcadedb/query/opencypher/procedures/algo/AlgoLouvain.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.arcadedb.query.sql.executor.Result;
2626
import com.arcadedb.query.sql.executor.ResultInternal;
2727

28+
import com.arcadedb.utility.IntIntHashMap;
29+
2830
import java.util.ArrayList;
2931
import java.util.HashMap;
3032
import java.util.Iterator;
@@ -201,7 +203,7 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
201203
final double finalModularity = computeModularity(vertices, community, vertexIndex, nodeDegree, totalWeight, weightProperty);
202204

203205
// Remap community IDs to be sequential starting from 0
204-
final Map<Integer, Integer> communityRemap = new HashMap<>();
206+
final IntIntHashMap communityRemap = new IntIntHashMap();
205207
int nextId = 0;
206208
for (int i = 0; i < n; i++) {
207209
if (!communityRemap.containsKey(community[i]))
@@ -211,7 +213,7 @@ public Stream<Result> execute(final Object[] args, final Result inputRow, final
211213
return IntStream.range(0, n).mapToObj(i -> {
212214
final ResultInternal result = new ResultInternal();
213215
result.setProperty("node", vertices.get(i).getIdentity());
214-
result.setProperty("communityId", communityRemap.get(community[i]));
216+
result.setProperty("communityId", communityRemap.get(community[i], -1));
215217
result.setProperty("modularity", finalModularity);
216218
return (Result) result;
217219
});

0 commit comments

Comments
 (0)