190 lines
6.6 KiB
Java
190 lines
6.6 KiB
Java
package unit;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* dart-0001/0002/0003: Dart2JS namedParameters List.contains() — O(N²) vs O(N)
|
|
*
|
|
* Simulates forEachOrderedParameterByFunctionNode from:
|
|
* pkg/compiler/lib/src/js_model/element_map.dart:636
|
|
* pkg/compiler/lib/src/ssa/builder.dart:2156, 5007
|
|
*
|
|
* The defective path uses List<String>.contains() inside an O(N) loop —
|
|
* total O(N²) equality comparisons.
|
|
* The fixed path converts to Set<String> once before the loop — O(1) per lookup.
|
|
*
|
|
* Test: N=500 named parameters, measures equality comparison counts.
|
|
* Expected ratio: >= 5x (in practice ~125x at N=500).
|
|
* Prints: N/N PASS
|
|
*/
|
|
public class DartTest {
|
|
|
|
static long slowOps = 0;
|
|
static long fastOps = 0;
|
|
|
|
/**
|
|
* Counted string wrapper — records each .equals() call.
|
|
*/
|
|
static class SlowName {
|
|
final String value;
|
|
|
|
SlowName(String v) { this.value = v; }
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (!(o instanceof SlowName)) return false;
|
|
slowOps++;
|
|
return this.value.equals(((SlowName) o).value);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
// Deliberately return constant hash so HashSet falls back to equals
|
|
// — but we don't use HashSet for slowOps; this is for List only.
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static class FastName {
|
|
final String value;
|
|
|
|
FastName(String v) { this.value = v; }
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (!(o instanceof FastName)) return false;
|
|
fastOps++;
|
|
return this.value.equals(((FastName) o).value);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return value.hashCode(); // proper hash — Set.contains() rarely calls equals()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Defective path:
|
|
* Build a List<SlowName> representing parameterStructure.namedParameters.
|
|
* For each of N parameter declarations, call listNames.contains(decl.name).
|
|
* Each call is O(N) → total O(N²).
|
|
*/
|
|
static void slowPath(int n) {
|
|
// parameterStructure.namedParameters (subset — all N are "live")
|
|
List<SlowName> structureNames = new ArrayList<>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
structureNames.add(new SlowName("param_" + i));
|
|
}
|
|
|
|
// node.namedParameters — all N parameter declarations
|
|
List<String> declaredNames = new ArrayList<>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
declaredNames.add("param_" + i);
|
|
}
|
|
|
|
// Defective filter: for each declared param, scan the list
|
|
for (String decl : declaredNames) {
|
|
SlowName probe = new SlowName(decl);
|
|
boolean isLive = structureNames.contains(probe); // O(N) each time
|
|
// use result to prevent dead-code elimination
|
|
if (!isLive) throw new RuntimeException("unexpected: param not found");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixed path:
|
|
* Convert parameterStructure.namedParameters to Set<FastName> once.
|
|
* Each contains() call is O(1).
|
|
*/
|
|
static void fastPath(int n) {
|
|
// parameterStructure.namedParameters → converted to Set once
|
|
Set<FastName> structureSet = new HashSet<>(n * 2);
|
|
for (int i = 0; i < n; i++) {
|
|
structureSet.add(new FastName("param_" + i));
|
|
}
|
|
|
|
// node.namedParameters — all N parameter declarations
|
|
List<String> declaredNames = new ArrayList<>(n);
|
|
for (int i = 0; i < n; i++) {
|
|
declaredNames.add("param_" + i);
|
|
}
|
|
|
|
// Fixed filter: O(1) contains per lookup
|
|
for (String decl : declaredNames) {
|
|
FastName probe = new FastName(decl);
|
|
boolean isLive = structureSet.contains(probe); // O(1)
|
|
if (!isLive) throw new RuntimeException("unexpected: param not found");
|
|
}
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
int N = 500;
|
|
int PASSES = 3;
|
|
|
|
// Warm-up (not counted)
|
|
slowPath(10);
|
|
fastPath(10);
|
|
slowOps = 0;
|
|
fastOps = 0;
|
|
|
|
// Measure
|
|
for (int p = 0; p < PASSES; p++) {
|
|
slowPath(N);
|
|
fastPath(N);
|
|
}
|
|
|
|
// Expected:
|
|
// slow: each of N items probed against list of N → N² comparisons per pass
|
|
// (worst case probe = not-found or found at end: N scans; avg = N/2)
|
|
// all N items are found (triangular: 1+2+...+N = N*(N+1)/2 per pass)
|
|
// Total for PASSES: PASSES * N*(N+1)/2 ≈ 3 * 125250 = 375750 for N=500
|
|
// fast: HashSet with distinct hashes → 0 equals() calls (hash uniquely determines bucket)
|
|
// In practice for N=500 unique names: ~0 equals calls
|
|
|
|
long expectedSlowMin = (long) PASSES * N * (N / 4); // conservative lower bound
|
|
long expectedFastMax = (long) PASSES * N * 2; // generous upper bound
|
|
|
|
System.out.println("N=" + N + " PASSES=" + PASSES);
|
|
System.out.println("slow ops (List.contains) : " + slowOps +
|
|
" expected >= " + expectedSlowMin);
|
|
System.out.println("fast ops (Set.contains) : " + fastOps +
|
|
" expected <= " + expectedFastMax);
|
|
|
|
int passed = 0;
|
|
int total = 3;
|
|
|
|
// Test 1: slow path shows O(N²) ops
|
|
if (slowOps >= expectedSlowMin) {
|
|
System.out.println("PASS 1/3: slow O(N²) confirmed — ops=" + slowOps + " >= " + expectedSlowMin);
|
|
passed++;
|
|
} else {
|
|
System.out.println("FAIL 1/3: slow ops=" + slowOps + " < expected " + expectedSlowMin);
|
|
}
|
|
|
|
// Test 2: fast path shows near-O(1) ops
|
|
if (fastOps <= expectedFastMax) {
|
|
System.out.println("PASS 2/3: fast O(1) confirmed — ops=" + fastOps + " <= " + expectedFastMax);
|
|
passed++;
|
|
} else {
|
|
System.out.println("FAIL 2/3: fast ops=" + fastOps + " > expected " + expectedFastMax);
|
|
}
|
|
|
|
// Test 3: ratio >= 5x
|
|
long ratio = (fastOps == 0) ? Long.MAX_VALUE : slowOps / fastOps;
|
|
boolean ratioOk = fastOps == 0 || slowOps >= fastOps * 5;
|
|
if (ratioOk) {
|
|
System.out.println("PASS 3/3: ratio=" + (fastOps == 0 ? "inf" : ratio) + "x >= 5x");
|
|
passed++;
|
|
} else {
|
|
System.out.println("FAIL 3/3: ratio=" + ratio + "x < 5x (slow=" + slowOps + " fast=" + fastOps + ")");
|
|
}
|
|
|
|
System.out.println(passed + "/" + total + " PASS");
|
|
if (passed != total) System.exit(1);
|
|
}
|
|
}
|