mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
perf(cfg): bound the reaching-defs fixpoint with a per-block visit ceiling (#2195)
The per-language verification swarm reproduced, AT PRODUCTION DEFAULTS, a reaching-defs blow-up: a machine-generated ~2000-line all-loops function (under DEFAULT_PDG_MAX_FUNCTION_LINES) reaches ~10k basic blocks because loops emit ~5 blocks/line, and the dataflow fixpoint is O(blocks^2.3) on deep loop nests — measured 62s (C/C++) and 2.05s + 810MB (Go) for ONE function. maxFacts does not help: the fact count stays LINEAR, so it never fires. Iterative reaching-defs on a reducible CFG converges in O(loop-nesting-depth) passes, so a worklist re-visits each block a small multiple of times for real code. Add a maxBlockVisits ceiling (emit passes blocks.length × 64 — far beyond any hand-written nesting depth, ~15) that bails when the fixpoint has not converged. An unconverged fixpoint's in/out sets are not sound, so it returns NO facts (status 'truncated', like the existing 'overflow' guard) — a per- function coverage gap, never wrong facts. Real code is byte-identical: full cfg suites 725 passed, bench --check fingerprints unchanged. NOTE: computeControlDependence's O(N²) up-walk on deep post-dom chains is the sibling concern but stays ~13ms in production (bounded by the line cap + the CDG materialization cap); a CDG work-budget is a documented follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6fed52e1a6
commit
cda4cac27f
3 changed files with 69 additions and 1 deletions
|
|
@ -95,6 +95,21 @@ export const REACHING_DEF_FACTS_PER_EDGE_CAP = 4;
|
|||
export const DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION =
|
||||
REACHING_DEF_FACTS_PER_EDGE_CAP * DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION;
|
||||
|
||||
/**
|
||||
* Fixpoint-iteration budget for {@link computeReachingDefs}, as a multiple of
|
||||
* the function's block count ({@link emitFileReachingDefs} passes
|
||||
* `blocks.length × this` as `maxBlockVisits`). Iterative reaching-defs on a
|
||||
* reducible CFG converges in O(loop-nesting-depth) passes, so a worklist
|
||||
* re-visits each block a small multiple of times for real code; this budget
|
||||
* tolerates a nesting depth far beyond any hand-written function (real code is
|
||||
* ≤ ~15 deep) while truncating the pathological deep nest that otherwise drives
|
||||
* the solver to O(blocks²) — measured at seconds + GB on a machine-generated
|
||||
* 2000-line all-loops function whose fact count stays linear (so `maxFacts`
|
||||
* never fires). Truncation degrades to a sound empty REACHING_DEF for that one
|
||||
* function (status `truncated`), never wrong facts.
|
||||
*/
|
||||
export const DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS = 64;
|
||||
|
||||
export interface CfgEmitResult {
|
||||
blocks: number;
|
||||
edges: number;
|
||||
|
|
@ -359,7 +374,10 @@ export function emitFileReachingDefs(
|
|||
);
|
||||
continue;
|
||||
}
|
||||
const r = computeReachingDefs(cfg, { maxFacts });
|
||||
const r = computeReachingDefs(cfg, {
|
||||
maxFacts,
|
||||
maxBlockVisits: cfg.blocks.length * DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS,
|
||||
});
|
||||
if (r.status === 'no-facts') continue;
|
||||
result.facts += r.facts.length;
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,19 @@ export interface ReachingDefsLimits {
|
|||
* `status: 'truncated'`. `undefined`/0 ⇒ unlimited.
|
||||
*/
|
||||
readonly maxFacts?: number;
|
||||
/**
|
||||
* Maximum total block dequeues in the dataflow fixpoint. Iterative
|
||||
* reaching-defs on a reducible CFG converges in O(loop-nesting-depth) passes,
|
||||
* so a worklist visits each block a small multiple of times for real code; a
|
||||
* pathologically deep loop nest (machine-generated / obfuscated) drives the
|
||||
* pass count — and thus the visit total — to O(blocks²) and the solver to
|
||||
* seconds + GB of heap (`maxFacts` does not help: fact count stays linear).
|
||||
* When the visit total exceeds this budget the fixpoint has NOT converged, so
|
||||
* any facts would be unsound — the solver bails to a sound empty
|
||||
* `status: 'truncated'` (like the `overflow` guard). `undefined`/0 ⇒ unlimited
|
||||
* (the default for direct callers; the emit path sets a per-function budget).
|
||||
*/
|
||||
readonly maxBlockVisits?: number;
|
||||
}
|
||||
|
||||
export interface FunctionDefUse {
|
||||
|
|
@ -222,11 +235,24 @@ export function computeReachingDefs(cfg: FunctionCfg, limits?: ReachingDefsLimit
|
|||
|
||||
const inWorklist = new Array(n).fill(true);
|
||||
let pending = n;
|
||||
// Fixpoint-iteration ceiling (see ReachingDefsLimits.maxBlockVisits): bound the
|
||||
// total block dequeues so a pathologically deep loop nest can't drive the
|
||||
// worklist to O(blocks²). undefined/0 ⇒ unlimited.
|
||||
const maxBlockVisits =
|
||||
limits?.maxBlockVisits && limits.maxBlockVisits > 0 ? limits.maxBlockVisits : Infinity;
|
||||
let blockVisits = 0;
|
||||
while (pending > 0) {
|
||||
for (const b of order) {
|
||||
if (!inWorklist[b]) continue;
|
||||
inWorklist[b] = false;
|
||||
pending -= 1;
|
||||
if (++blockVisits > maxBlockVisits) {
|
||||
// Did NOT converge within the budget — the in/out sets are not at the
|
||||
// fixpoint, so any facts would be unsound. Bail to a sound empty
|
||||
// `truncated` result (a coverage gap, not an error), carrying the def/use
|
||||
// telemetry already gathered.
|
||||
return { status: 'truncated', bindings: cfg.bindings, facts: [], defCount, useCount };
|
||||
}
|
||||
|
||||
const p = preds[b];
|
||||
const inB: Lattice =
|
||||
|
|
|
|||
|
|
@ -347,6 +347,30 @@ describe('computeReachingDefs — determinism and convergence', () => {
|
|||
expect(capped.defCount).toBe(full.defCount);
|
||||
expect(capped.useCount).toBe(full.useCount);
|
||||
});
|
||||
|
||||
it('maxBlockVisits ceiling: a budget below convergence bails to a sound empty truncated', () => {
|
||||
// entry → body (self-loop, forces re-processing) → exit; body defs+uses x.
|
||||
const blocks: BlockSpec[] = [{}, {}, { stmts: [stmt(3, [0], [0])] }];
|
||||
const edges: [number, number][] = [
|
||||
[0, 2],
|
||||
[2, 2], // self-loop → the fixpoint re-visits block 2
|
||||
[2, 1],
|
||||
];
|
||||
// Unbounded (and a generous budget) converge with the loop-carried fact.
|
||||
const full = computeReachingDefs(mkCfg(blocks, edges, ['x']));
|
||||
expect(full.status).toBe('computed');
|
||||
expect(full.facts.length).toBeGreaterThan(0);
|
||||
const budgeted = computeReachingDefs(mkCfg(blocks, edges, ['x']), { maxBlockVisits: 1000 });
|
||||
expect(budgeted.status).toBe('computed');
|
||||
expect(render(budgeted.facts)).toEqual(render(full.facts)); // byte-identical for normal code
|
||||
|
||||
// A budget below convergence cannot reach the fixpoint, so facts would be
|
||||
// unsound → return NONE (sound), status 'truncated', telemetry preserved.
|
||||
const capped = computeReachingDefs(mkCfg(blocks, edges, ['x']), { maxBlockVisits: 1 });
|
||||
expect(capped.status).toBe('truncated');
|
||||
expect(capped.facts).toEqual([]);
|
||||
expect(capped.defCount).toBe(full.defCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeReachingDefs — parser-direct acceptance (with U1/U2)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue