mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
perf(cfg): bound the SSA value-graph, fall back to dense when oversized (#2201 review R1)
maxFacts bounds fact materialization in sweepFacts, but nothing bounded the SSA-sparse solver's φ/value-graph construction. A high-binding-density deep loop routed to SSA (≥16 blocks + a reachable loop) builds an O(blocks×bindings) value graph the dense path would have truncated at its maxBlockVisits ceiling (~1.5 GB measured on a 3000-block × 300-binding function). Cap the value graph: after φ-placement (where nodeKeys.length == the φ count, the input-superlinear term) plus a 2×Σgen bound on the renaming nodes, fall back to computeInSetsDense before paying for renaming + Tarjan SCC. The fallback is byte-identical (dense is the equivalence oracle) and bounded (dense honors maxBlockVisits). Mirrors the existing throw/unreachable/OOB-binding gates. The ceiling is DEFAULT_MAX_SSA_VALUE_GRAPH_NODES (1e6 — far above any real or benchmarked function; dense-bindings/deep-nest build <1e4), overridable per call via ReachingDefsLimits.maxSsaValueGraphNodes. The new unit test makes the otherwise-invisible routing flip observable by pairing the cap with a tight maxBlockVisits (dense truncates, SSA computes). Equivalence fuzz unchanged (byte-identical, 20k CFGs green); tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7e78026e1a
commit
2c344ddfeb
2 changed files with 104 additions and 0 deletions
|
|
@ -103,6 +103,19 @@ export interface ReachingDefsLimits {
|
|||
* a per-function budget).
|
||||
*/
|
||||
readonly maxBlockVisits?: number;
|
||||
/**
|
||||
* Memory bound on the SSA-sparse solver's value-graph construction (#2201
|
||||
* review R1). `maxFacts` bounds fact MATERIALIZATION (sweepFacts) but nothing
|
||||
* bounds the φ/value-graph the sparse path builds first; a high-binding-density
|
||||
* deep loop routed to SSA (≥ SSA_MIN_BLOCKS blocks + a reachable loop) builds an
|
||||
* O(blocks×bindings) graph the dense path would have truncated at the
|
||||
* `maxBlockVisits` ceiling (~1.5 GB measured on a 3000-block × 300-binding
|
||||
* function). When the projected node count would exceed this, the sparse solver
|
||||
* falls back to the dense oracle (byte-identical, and bounded — dense honors
|
||||
* `maxBlockVisits`). Honored ONLY by the sparse path; the dense solver ignores
|
||||
* it. `undefined`/0 ⇒ {@link DEFAULT_MAX_SSA_VALUE_GRAPH_NODES}.
|
||||
*/
|
||||
readonly maxSsaValueGraphNodes?: number;
|
||||
}
|
||||
|
||||
export interface FunctionDefUse {
|
||||
|
|
@ -664,6 +677,25 @@ function computeInSetsSparse(
|
|||
}
|
||||
}
|
||||
|
||||
// ── memory bound (#2201 review R1): cap the value graph, else fall back ──
|
||||
// After φ-placement, nodeKeys.length == the φ-node count — the term that grows
|
||||
// superlinearly with the input on the deep-loop / dense-binding pathology.
|
||||
// Renaming below adds at most ~2 nodes per gen entry (already bounded by the
|
||||
// def-site universe the STMT_STRIDE overflow guard caps). If the projected
|
||||
// total would exceed the budget, fall back to the dense oracle here — BEFORE
|
||||
// paying for renaming + Tarjan SCC on a blown-up graph. Byte-identical (dense
|
||||
// is the equivalence oracle) and bounded (dense honors maxBlockVisits). Mirrors
|
||||
// the throw-edge / unreachable / OOB-binding gates at the top of this function.
|
||||
const nodeBudget =
|
||||
limits?.maxSsaValueGraphNodes && limits.maxSsaValueGraphNodes > 0
|
||||
? limits.maxSsaValueGraphNodes
|
||||
: DEFAULT_MAX_SSA_VALUE_GRAPH_NODES;
|
||||
let projectedRenameNodes = 0;
|
||||
for (let b = 0; b < n; b++) projectedRenameNodes += (gen[b]?.size ?? 0) * 2;
|
||||
if (nodeKeys.length + projectedRenameNodes > nodeBudget) {
|
||||
return computeInSetsDense(cfg, n, h, adj, limits);
|
||||
}
|
||||
|
||||
// ── renaming (iterative dominator-tree DFS, per-binding value stacks) ──
|
||||
const domChildren: number[][] = Array.from({ length: nx }, () => []);
|
||||
for (let b = 0; b < nx; b++) if (b !== S && idom[b] !== -1) domChildren[idom[b]].push(b);
|
||||
|
|
@ -825,6 +857,21 @@ function computeInSetsSparse(
|
|||
*/
|
||||
const SSA_MIN_BLOCKS = 16;
|
||||
|
||||
/**
|
||||
* Default ceiling on the SSA-sparse solver's value-graph node count (#2201
|
||||
* review R1). Above this the sparse path falls back to the dense oracle (which
|
||||
* bounds its own work via `maxBlockVisits`), trading the deep-loop full-facts
|
||||
* win for bounded memory on pathological inputs. Sized FAR above any real or
|
||||
* benchmarked function: the suite's densest SSA scenarios (`dense-bindings`,
|
||||
* `deep-nest`) build well under 10⁴ nodes, while the pathology this guards
|
||||
* (thousands of blocks × hundreds of bindings) builds 10⁶–10⁷. The
|
||||
* `dense-bindings` / `deep-nest` `rd_scaling_budget` gates in
|
||||
* bench/cfg/baselines.json fail if this is set so low it forces those scenarios
|
||||
* onto the dense path. Overridable per-call via
|
||||
* {@link ReachingDefsLimits.maxSsaValueGraphNodes}.
|
||||
*/
|
||||
const DEFAULT_MAX_SSA_VALUE_GRAPH_NODES = 1_000_000;
|
||||
|
||||
/**
|
||||
* True iff a cycle is reachable from `entry` (the CFG has a loop). Iterative DFS
|
||||
* with a gray/black coloring; a gray successor is a back-edge. O(V+E).
|
||||
|
|
|
|||
|
|
@ -425,6 +425,63 @@ describe('computeReachingDefs — determinism and convergence', () => {
|
|||
expect(prod!.status).toBe(dense.status);
|
||||
expect(render(prod!.facts)).toEqual(render(dense.facts)); // byte-identical to the tolerant dense path
|
||||
});
|
||||
|
||||
it('#2201 R1: an oversized SSA value graph falls back to the dense oracle (byte-identical)', () => {
|
||||
// A ≥16-block looping multi-binding CFG → the production dispatcher routes it
|
||||
// to the SSA-sparse path. `maxFacts` bounds only fact materialization, not the
|
||||
// φ/value-graph the sparse path builds first; `maxSsaValueGraphNodes` caps that
|
||||
// graph and falls back to the dense oracle when it would be too large. Because
|
||||
// the fallback is byte-identical to dense, the routing flip is made OBSERVABLE
|
||||
// via a tight `maxBlockVisits`: dense honors the ceiling (truncates), the SSA
|
||||
// path ignores it (computes) — so the same budget yields different statuses
|
||||
// depending on which solver ran.
|
||||
const K = 4; // bindings
|
||||
const blocks: BlockSpec[] = [{}, {}]; // 0 entry, 1 exit
|
||||
const edges: [number, number][] = [[0, 2]];
|
||||
const BODY = 18; // body blocks 2..19 → 20 blocks total (≥ SSA_MIN_BLOCKS)
|
||||
for (let i = 0; i < BODY; i++) {
|
||||
const b = 2 + i;
|
||||
blocks[b] = { stmts: [stmt(b * 10, [i % K], [(i + 1) % K])] };
|
||||
if (i < BODY - 1) edges.push([b, b + 1]);
|
||||
}
|
||||
edges.push([2 + BODY - 1, 2]); // back-edge → reachable loop (forces SSA dispatch)
|
||||
edges.push([2, 1]); // exit
|
||||
const bindings = Array.from({ length: K }, (_, i) => `v${i}`);
|
||||
const mk = () => mkCfg(blocks, edges, bindings);
|
||||
expect(mk().blocks.length).toBeGreaterThanOrEqual(16);
|
||||
|
||||
const denseFull = computeReachingDefsDense(mk());
|
||||
expect(denseFull.status).toBe('computed');
|
||||
expect(denseFull.facts.length).toBeGreaterThan(0);
|
||||
|
||||
// Tiny node cap, unbounded visits → falls back to dense → byte-identical.
|
||||
const cappedUnbounded = computeReachingDefs(mk(), { maxSsaValueGraphNodes: 1 });
|
||||
expect(cappedUnbounded.status).toBe(denseFull.status);
|
||||
expect(render(cappedUnbounded.facts)).toEqual(render(denseFull.facts));
|
||||
|
||||
// Tiny node cap + tight block-visit budget → fallback to dense, whose ceiling
|
||||
// then fires (truncated, empty). This is the observable proof the cap diverted
|
||||
// the solve to the dense path.
|
||||
const cappedBudgeted = computeReachingDefs(mk(), {
|
||||
maxSsaValueGraphNodes: 1,
|
||||
maxBlockVisits: 1,
|
||||
});
|
||||
expect(cappedBudgeted.status).toBe('truncated');
|
||||
expect(cappedBudgeted.facts).toEqual([]);
|
||||
|
||||
// Default (huge) cap + the SAME tight budget → SSA path runs (no fixpoint
|
||||
// iteration → ceiling never fires) and computes the full facts.
|
||||
const uncapped = computeReachingDefs(mk(), { maxBlockVisits: 1 });
|
||||
expect(uncapped.status).toBe('computed');
|
||||
expect(render(uncapped.facts)).toEqual(render(denseFull.facts));
|
||||
|
||||
// Boundary monotonicity: a cap well above the graph stays on SSA (computes
|
||||
// under the tight budget), a cap well below falls back (truncates).
|
||||
const above = computeReachingDefs(mk(), { maxSsaValueGraphNodes: 100_000, maxBlockVisits: 1 });
|
||||
expect(above.status).toBe('computed');
|
||||
const below = computeReachingDefs(mk(), { maxSsaValueGraphNodes: 5, maxBlockVisits: 1 });
|
||||
expect(below.status).toBe('truncated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeReachingDefs — parser-direct acceptance (with U1/U2)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue