172 lines
6 KiB
Java
172 lines
6 KiB
Java
package unit;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.HashMap;
|
||
import java.util.LinkedHashSet;
|
||
import java.util.LinkedList;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
|
||
/**
|
||
* neo4j-0001: Dijkstra predecessors List.contains() — O(E×P) vs O(E)
|
||
*
|
||
* Simulates the predecessor deduplication guard from Dijkstra.java:324.
|
||
* The defective path uses List<String> (ArrayList.contains = O(N)).
|
||
* The fixed path uses Set<String> (HashSet.contains = O(1)).
|
||
*
|
||
* Each "relationship" is a unique String object.
|
||
* We measure how many equality comparisons .contains() triggers
|
||
* by wrapping items in a counted comparator object.
|
||
*/
|
||
public class Neo4jTest {
|
||
|
||
static int slowContainsOps = 0;
|
||
static int fastContainsOps = 0;
|
||
|
||
static class CountedRel {
|
||
final int id;
|
||
CountedRel(int id) { this.id = id; }
|
||
|
||
@Override
|
||
public boolean equals(Object o) {
|
||
if (this == o) return true;
|
||
if (!(o instanceof CountedRel)) return false;
|
||
// Count each comparison
|
||
slowContainsOps++;
|
||
return this.id == ((CountedRel) o).id;
|
||
}
|
||
|
||
@Override
|
||
public int hashCode() {
|
||
return id;
|
||
}
|
||
}
|
||
|
||
static class CountedRelFast {
|
||
final int id;
|
||
CountedRelFast(int id) { this.id = id; }
|
||
|
||
@Override
|
||
public boolean equals(Object o) {
|
||
if (this == o) return true;
|
||
if (!(o instanceof CountedRelFast)) return false;
|
||
fastContainsOps++;
|
||
return this.id == ((CountedRelFast) o).id;
|
||
}
|
||
|
||
@Override
|
||
public int hashCode() {
|
||
return id; // proper hash — Set.contains() can short-circuit
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Defective path: List<Rel> predecessors.
|
||
* For N predecessors, each contains() call costs O(N) in the worst case
|
||
* (item not present triggers full scan).
|
||
*/
|
||
static void slowPath(int n) {
|
||
Map<Integer, List<CountedRel>> predecessors = new HashMap<>();
|
||
// Simulate adding n unique relationships to the predecessor list for node 0
|
||
predecessors.put(0, new ArrayList<>());
|
||
for (int i = 0; i < n; i++) {
|
||
CountedRel rel = new CountedRel(i);
|
||
List<CountedRel> preds = predecessors.get(0);
|
||
// This is the defective guard: O(P) scan per call
|
||
if (!preds.contains(rel)) {
|
||
preds.add(rel);
|
||
}
|
||
}
|
||
// Now simulate a second pass: trying to add the same rels again (duplicates)
|
||
// Each contains() now scans all n existing entries
|
||
for (int i = 0; i < n; i++) {
|
||
CountedRel rel = new CountedRel(i);
|
||
List<CountedRel> preds = predecessors.get(0);
|
||
if (!preds.contains(rel)) {
|
||
preds.add(rel);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fixed path: Set<Rel> predecessors.
|
||
* HashSet.contains() is O(1) — hash lookup, equals only called on collision.
|
||
*/
|
||
static void fastPath(int n) {
|
||
Map<Integer, Set<CountedRelFast>> predecessors = new HashMap<>();
|
||
predecessors.put(0, new LinkedHashSet<>());
|
||
for (int i = 0; i < n; i++) {
|
||
CountedRelFast rel = new CountedRelFast(i);
|
||
Set<CountedRelFast> preds = predecessors.get(0);
|
||
preds.add(rel); // Set.add() deduplicates; no explicit contains() needed
|
||
}
|
||
// Duplicate pass: set semantics are idempotent
|
||
for (int i = 0; i < n; i++) {
|
||
CountedRelFast rel = new CountedRelFast(i);
|
||
predecessors.get(0).add(rel);
|
||
}
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
int N = 500;
|
||
int PASSES = 3;
|
||
|
||
// Warm up
|
||
slowPath(10);
|
||
fastPath(10);
|
||
slowContainsOps = 0;
|
||
fastContainsOps = 0;
|
||
|
||
// Measure
|
||
for (int p = 0; p < PASSES; p++) {
|
||
slowPath(N);
|
||
fastPath(N);
|
||
}
|
||
|
||
// For the slow path: first pass adds N items with 0..N-1 scans = N*(N-1)/2 ops
|
||
// Second pass finds all N items present: each scan goes full N = N*N ops
|
||
// Total per call: ~N^2/2 + N^2 = ~1.5*N^2 comparisons
|
||
// For fast path: hash collisions are rare; ideally ~0 equals() calls for unique ids
|
||
|
||
long expectedSlowMin = (long)(N * N / 4); // conservative lower bound per pass
|
||
long expectedFastMax = (long)(N * PASSES * 2); // generous upper bound
|
||
|
||
System.out.println("N=" + N + " PASSES=" + PASSES);
|
||
System.out.println("slow contains ops : " + slowContainsOps + " (expected >=" + expectedSlowMin + ")");
|
||
System.out.println("fast contains ops : " + fastContainsOps + " (expected <=" + expectedFastMax + ")");
|
||
|
||
int passed = 0;
|
||
int total = 0;
|
||
|
||
total++;
|
||
if (slowContainsOps >= expectedSlowMin) {
|
||
System.out.println("PASS 1/" + total + ": slow path O(n^2) confirmed (ops=" + slowContainsOps + " >= " + expectedSlowMin + ")");
|
||
passed++;
|
||
} else {
|
||
System.out.println("FAIL 1/" + total + ": slow path did not show expected O(n^2) ops");
|
||
}
|
||
|
||
total++;
|
||
if (fastContainsOps <= expectedFastMax) {
|
||
System.out.println("PASS 2/" + total + ": fast path O(1) confirmed (ops=" + fastContainsOps + " <= " + expectedFastMax + ")");
|
||
passed++;
|
||
} else {
|
||
System.out.println("FAIL 2/" + total + ": fast path showed too many comparisons: " + fastContainsOps);
|
||
}
|
||
|
||
total++;
|
||
// Ratio must be at least 10x
|
||
boolean ratioOk = slowContainsOps >= fastContainsOps * 10;
|
||
if (fastContainsOps == 0 || ratioOk) {
|
||
System.out.println("PASS 3/" + total + ": ratio slow/fast is " +
|
||
(fastContainsOps == 0 ? "inf" : (slowContainsOps / fastContainsOps)) + "x (expected >=10x)");
|
||
passed++;
|
||
} else {
|
||
System.out.println("FAIL 3/" + total + ": ratio too small: slow=" + slowContainsOps + " fast=" + fastContainsOps);
|
||
}
|
||
|
||
System.out.println(passed + "/" + total + " PASS");
|
||
if (passed != total) System.exit(1);
|
||
}
|
||
}
|