Skip to content

Instantly share code, notes, and snippets.

@MattPD
Created August 8, 2026 05:34
Show Gist options
  • Select an option

  • Save MattPD/a2910ca82c86333dba0181a29bf79526 to your computer and use it in GitHub Desktop.

Select an option

Save MattPD/a2910ca82c86333dba0181a29bf79526 to your computer and use it in GitHub Desktop.
LoopInterchange inner-subnest design walkthrough

LoopInterchange: route inner subnests inside non-linear loop trees

#214920 | 4a6f6018

Context: Improving the SPECfp2000/SWIM2000 benchmark performance for Flang.

Background: SPEC CPU2000 171.swim does not interchange under Flang/LLVM (related earlier work & comment: sebpop reported that it did not interchange under Flang because LLVM could not infer the number of iterations).

This patch addresses a different blocker in the same benchmark. Section 5 states what remains.

Patch symbols live in LoopInterchange.cpp. Named test cases are bfs_loop_count_is_not_depth, dep_unknown_ancestor, disjoint_costmodel, and deep_spine_wide_siblings.

1. Problem

LoopInterchange treats a breadth-first list of sibling-rich descendants as one linear nest, so an unrelated sibling or uncomputable ancestor can prevent a legal, profitable inner pair from ever reaching the pass's existing legality and profitability checks.

The motivating SPEC CPU2000 171.swim shallow-water benchmark accumulates three scalar checksums over a two-dimensional index range in one loop nest. The nest iterates the first Fortran subscript in the outer loop, so its innermost loop does not run at unit stride. Interchanging the two loops moves the first subscript into the innermost loop. That traversal then runs at unit stride. A reduced SWIM-inspired shape, not verbatim benchmark source, is:

do i = 1, n
  do j = 1, n
    check_u = check_u + u(i,j)
    check_v = check_v + v(i,j)
    check_p = check_p + p(i,j)
  end do
  u(i,i) = update_diagonal(u(i,i))
end do

Fortran stores the first subscript contiguously. With j as the inner loop, u(i,j) advances by one complete column, 1335 elements in the benchmark's arrays. The LLVM test case expresses that address as [1335 x double] indexed by %j, %i at L305-L313 (see below). Interchanging the loops makes %i, the unit-stride dimension, innermost.

The diagonal update after the inner loop makes the source nest imperfect. A hand-applied distribution separates the checksum reduction from the diagonal update:

do i = 1, n
  do j = 1, n
    check_u = check_u + u(i,j)
    check_v = check_v + v(i,j)
    check_p = check_p + p(i,j)
  end do
end do

do i = 1, n
  u(i,i) = update_diagonal(u(i,i))
end do

The current change handles the candidate-routing problem after the reduction pair is available. Even then, Flang presents that pair inside a larger loop tree with sibling loops and runtime trip counts:

top
  checksum_outer
    checksum_inner
  sibling_1
  sibling_2
  ...

The pair already satisfies LoopInterchange's reduction, legality, and default profitability rules. LoopNest::getLoops() returns the root and descendants in breadth-first order. Its size counts every loop in the tree, so siblings can increase it beyond the maximum nesting depth. In the canonical regression, the count is 12 while the true depth is 3. The -loop-interchange-max-loop-nest-depth default is 10, so the old pass bails before pair legality runs.

This patch changes candidate routing. It does not add loop distribution, a swim pattern, or new recurrence semantics. Follow-on patches distribute the outer epilogue and handle runtime outer bounds.

2. Position in the compilation pipeline

The production change is confined to llvm/lib/Transforms/Scalar/LoopInterchange.cpp. Flang contributes ordinary LLVM IR. The pass runs through the existing -floop-interchange driver option, unchanged by this patch. No Fortran semantic, HLFIR, FIR, MLIR, target, or runtime layer changes.

Correctness depends on four surrounding contracts:

  1. LoopInterchangePass::run must distinguish true nesting depth, a linear standard-path chain, and a non-linear loop tree.
  2. tryInnerSubnestFallback must select one bounded adjacent pair without dropping enclosing dependence dimensions.
  3. CacheCostManager must judge that pair from its ancestor chain, not a disjoint sibling.
  4. The interchange transform must preserve the selected loop's slot in its parent's LoopInfo child list. This patch adds that guarantee.

3. Design rationale

3.1 Select only a direct parent and leaf child

The fallback accepts an outer loop with exactly one child when that child is innermost. This shape lets the pass reuse the current two-loop transformation and its canonical-form assumptions. The selected loops occupy the last two columns of the dependence matrix described in Section 3.2.

The rejected LoopNest::getPerfectLoops() alternative has the wrong eligibility contract. Its perfect-nest test rejects ordinary reduction nests that LoopInterchange's own legality accepts, because those nests hoist address arithmetic between the loops. Using it would still reject the reduction nest before LoopInterchange's own legality check, so the valid pair would remain unoptimized.

Arbitrary inner subtrees were also rejected. A non-leaf child introduces another choice about which descendant moves, whether more than one swap occurs, and which profitability decision governs the result. The leaf restriction keeps one candidate equal to one existing adjacent-loop interchange.

3.2 Keep every enclosing dependence dimension

DependenceAnalysis direction-vector levels are absolute loop depths. The fallback therefore builds the complete chain from the true outermost root to the candidate inner loop. It maps the selected outer and inner loops to the last two matrix columns.

Projecting each dependence onto two candidate columns was rejected as unsound. An unknown outer direction followed by a legal-looking inner pair can still forbid the permutation. The dep_unknown_ancestor test case pins that rejection. A < at an enclosing level already carries the dependence outside the candidate pair, so the row is discharged. An = carries nothing, so scanning continues inward. A * or > leaves the enclosing order unproven, so the candidate is rejected. This prefix rule is conservative policy. Existing pair legality still decides the selected columns after an equal prefix.

Scanning memory from the complete root was rejected for the opposite reason. Sibling operations are not reordered by the candidate swap. Feeding them into the matrix can create unrelated all-direction rows and suppress a valid pair. populateDependencyMatrix therefore receives the candidate outer loop as the memory subtree while using the full ancestor count as matrix width.

3.3 Build cache cost from the candidate chain

Whole-nest LoopCacheAnalysis collects loops breadth-first. getInnerMostLoop uses the last loop when the depths remain ordered, and populateReferenceGroups builds reference groups from that loop's blocks. In a non-linear tree, the last breadth-first loop can belong to a sibling subtree disjoint from the selected pair. Reusing that cost for the selected pair lets unrelated references determine profitability.

Blanket cache abstention was rejected because it removed correct cache-based decisions. Reconstructing LoopCacheAnalysis's internal innermost-loop choice was rejected because it coupled the fallback to an implementation detail. CacheCostManager instead accepts the explicit root-to-candidate chain. The disjoint_costmodel regression fails if sibling memory influences the selected pair's cache-cost decision.

3.4 Bound work without restoring a width cliff

The hidden -loop-interchange-max-inner-subnest-candidates option sets the attempt budget and defaults to ten. Enumeration visits every descendant once and keeps a bounded ordered list containing at most budget + 1 candidates.

(Ten is a policy default, not a legality threshold or a claimed optimum. It also matches the existing maximum-nest-depth default. The compile-time measurements in Section 7 validate the shipped cap's cost; they do not compare nearby default values. The hidden option keeps the cap tunable.)

Enumeration orders candidates deepest first and preserves breadth-first order among equal-depth candidates. The extra entry is the overflow marker. The pass attempts candidates in order and stops after one successful interchange.

Each attempt builds its own dependence matrix and cache-cost manager, and eligible fallback pairs never overlap or nest. Stopping after one success is therefore a policy boundary, not a stale-analysis requirement. The existing linear path performs multiple swaps to choose an ordering within one nest. The fallback instead sees independent pairs across a non-linear tree. Transforming several would need policy for pair ordering, aggregate profitability, revisiting a transformed pair, and termination. It would also add compile time. This patch leaves that policy to future work and bounds each invocation to one fallback transform.

A cap on total descendants was considered and removed. Such a cap limits cheap enumeration rather than expensive dependence work. It also recreates the original bug: adding harmless sibling loops would disable a valid inner pair. The attempt cap and the existing memory-ratio prefilter bound expensive work without making sibling count an eligibility policy.

Calling Loop::getLoopDepth() for every candidate was also rejected. That call walks the parent chain, so it costs depth times descendants. The final InnerSubnestCandidate stores depth carried through one breadth-first worklist. Enumeration costs O(descendants * (budget + 1)) time because each descendant can enter the bounded ordered list. The worklist uses O(descendants) auxiliary storage. The independently rebuilt ancestor chain asserts the carried depth before dependence analysis.

The deep_spine_wide_siblings test lowers the budget to seven. Distinct debug locations distinguish seven ordered attempts and the eighth overflow marker. The test catches lost breadth, reversed tie order, wrong depth, and incorrect budget retention.

3.5 Route non-linear nests directly

Every non-linear nest reaches the fallback regardless of whether a whole-list ScalarEvolution walk succeeds. Performing that walk before fallback cannot change dispatch, so the final routing skips it.

A linear list that is not SCEV-computable takes the same route. One uncomputable enclosing loop no longer discards a computable inner pair.

The standard Computed dependence info remark now belongs only to the linear standard path. Preserving it on non-linear input was rejected because it claimed that the complete breadth-first list was a transform candidate when it was not. The hidden -loop-interchange-enable-inner-subnest-fallback option defaults to true. Setting it to false disables only fallback transformation. Corrected depth handling, non-linear dispatch, and remark ownership remain active.

3.6 Preserve LoopInfo sibling order

The existing transform removes the old outer loop from its parent and installs the new outer loop. Appending the replacement moves it behind later siblings. That ordering defect changes LoopPassManager visitation order although the CFG remains unchanged.

For nested loops, replaceChildLoopWith preserves the original slot. The standard linear path has one child, so the change is behavior-equivalent there. The bfs_loop_count_is_not_depth loop dump checks the exact post-transform order.

3.7 Harden shared dependence guards

Two shared checks are hardened before routing changes. The memory-ratio filter now uses 64-bit products. A wrapped ratio product can spuriously reject a nest, while a wrapped squared memory count can bypass the filter. A large-ratio regression test pins the spurious-rejection failure mode.

The same hardening rejects a direction vector longer than the analyzed chain. Current callers construct complete chains, so the assertion never fires today. Without the release-build rejection, a future caller that broke the invariant would make the padding loop non-terminating.

4. Implementation walkthrough

Use bfs_loop_count_is_not_depth as the canonical example. It reproduces the Section 1 shape in minimal form: one profitable inner pair beside unrelated sibling loops under a single root.

The complete test case appears below. Line numbers refer to llvm/test/Transforms/LoopInterchange/inner-subnest-candidates.ll, where the function spans lines 285 through 425.

@bfs_loop_count_is_not_depth
L285: define void @bfs_loop_count_is_not_depth(ptr %A, ptr %B, ptr %C, ptr %R) {
L286: entry:
L287:   br label %top.header
L288: 
L289: top.header:
L290:   %t = phi i64 [ 0, %entry ], [ %t.next, %top.latch ]
L291:   br label %pairX.outer.header
L292: 
L293: pairX.outer.header:
L294:   %i = phi i64 [ 0, %top.header ], [ %i.next, %pairX.outer.latch ]
L295:   %sumA.i = phi double [ 0.000000e+00, %top.header ], [ %sumA.i.lcssa, %pairX.outer.latch ]
L296:   %sumB.i = phi double [ 0.000000e+00, %top.header ], [ %sumB.i.lcssa, %pairX.outer.latch ]
L297:   %sumC.i = phi double [ 0.000000e+00, %top.header ], [ %sumC.i.lcssa, %pairX.outer.latch ]
L298:   br label %pairX.inner
L299: 
L300: pairX.inner:
L301:   %j = phi i64 [ 0, %pairX.outer.header ], [ %j.next, %pairX.inner ]
L302:   %sumA.j = phi double [ %sumA.i, %pairX.outer.header ], [ %sumA.j.next, %pairX.inner ]
L303:   %sumB.j = phi double [ %sumB.i, %pairX.outer.header ], [ %sumB.j.next, %pairX.inner ]
L304:   %sumC.j = phi double [ %sumC.i, %pairX.outer.header ], [ %sumC.j.next, %pairX.inner ]
L305:   %idxA = getelementptr inbounds [1335 x double], ptr %A, i64 %j, i64 %i
L306:   %a = load double, ptr %idxA, align 8
L307:   %sumA.j.next = fadd reassoc double %sumA.j, %a
L308:   %idxB = getelementptr inbounds [1335 x double], ptr %B, i64 %j, i64 %i
L309:   %b = load double, ptr %idxB, align 8
L310:   %sumB.j.next = fadd reassoc double %sumB.j, %b
L311:   %idxC = getelementptr inbounds [1335 x double], ptr %C, i64 %j, i64 %i
L312:   %c = load double, ptr %idxC, align 8
L313:   %sumC.j.next = fadd reassoc double %sumC.j, %c
L314:   %j.next = add i64 %j, 1
L315:   %j.ec = icmp eq i64 %j.next, 1335
L316:   br i1 %j.ec, label %pairX.outer.latch, label %pairX.inner
L317: 
L318: pairX.outer.latch:
L319:   %sumA.i.lcssa = phi double [ %sumA.j.next, %pairX.inner ]
L320:   %sumB.i.lcssa = phi double [ %sumB.j.next, %pairX.inner ]
L321:   %sumC.i.lcssa = phi double [ %sumC.j.next, %pairX.inner ]
L322:   %i.next = add i64 %i, 1
L323:   %i.ec = icmp eq i64 %i.next, 1335
L324:   br i1 %i.ec, label %pairX.exit, label %pairX.outer.header
L325: 
L326: pairX.exit:
L327:   %sumA.live = phi double [ %sumA.i.lcssa, %pairX.outer.latch ]
L328:   %sumB.live = phi double [ %sumB.i.lcssa, %pairX.outer.latch ]
L329:   %sumC.live = phi double [ %sumC.i.lcssa, %pairX.outer.latch ]
L330:   %rB = getelementptr inbounds double, ptr %R, i64 1
L331:   %rC = getelementptr inbounds double, ptr %R, i64 2
L332:   store double %sumA.live, ptr %R, align 8
L333:   store double %sumB.live, ptr %rB, align 8
L334:   store double %sumC.live, ptr %rC, align 8
L335:   br label %sib1.header
L336: 
L337: sib1.header:
L338:   %s1 = phi i64 [ 0, %pairX.exit ], [ %s1.next, %sib1.header ]
L339:   %s1.next = add i64 %s1, 1
L340:   %s1.ec = icmp eq i64 %s1.next, 4
L341:   br i1 %s1.ec, label %sib1.exit, label %sib1.header
L342: 
L343: sib1.exit:
L344:   br label %sib2.header
L345: 
L346: sib2.header:
L347:   %s2 = phi i64 [ 0, %sib1.exit ], [ %s2.next, %sib2.header ]
L348:   %s2.next = add i64 %s2, 1
L349:   %s2.ec = icmp eq i64 %s2.next, 4
L350:   br i1 %s2.ec, label %sib2.exit, label %sib2.header
L351: 
L352: sib2.exit:
L353:   br label %sib3.header
L354: 
L355: sib3.header:
L356:   %s3 = phi i64 [ 0, %sib2.exit ], [ %s3.next, %sib3.header ]
L357:   %s3.next = add i64 %s3, 1
L358:   %s3.ec = icmp eq i64 %s3.next, 4
L359:   br i1 %s3.ec, label %sib3.exit, label %sib3.header
L360: 
L361: sib3.exit:
L362:   br label %sib4.header
L363: 
L364: sib4.header:
L365:   %s4 = phi i64 [ 0, %sib3.exit ], [ %s4.next, %sib4.header ]
L366:   %s4.next = add i64 %s4, 1
L367:   %s4.ec = icmp eq i64 %s4.next, 4
L368:   br i1 %s4.ec, label %sib4.exit, label %sib4.header
L369: 
L370: sib4.exit:
L371:   br label %sib5.header
L372: 
L373: sib5.header:
L374:   %s5 = phi i64 [ 0, %sib4.exit ], [ %s5.next, %sib5.header ]
L375:   %s5.next = add i64 %s5, 1
L376:   %s5.ec = icmp eq i64 %s5.next, 4
L377:   br i1 %s5.ec, label %sib5.exit, label %sib5.header
L378: 
L379: sib5.exit:
L380:   br label %sib6.header
L381: 
L382: sib6.header:
L383:   %s6 = phi i64 [ 0, %sib5.exit ], [ %s6.next, %sib6.header ]
L384:   %s6.next = add i64 %s6, 1
L385:   %s6.ec = icmp eq i64 %s6.next, 4
L386:   br i1 %s6.ec, label %sib6.exit, label %sib6.header
L387: 
L388: sib6.exit:
L389:   br label %sib7.header
L390: 
L391: sib7.header:
L392:   %s7 = phi i64 [ 0, %sib6.exit ], [ %s7.next, %sib7.header ]
L393:   %s7.next = add i64 %s7, 1
L394:   %s7.ec = icmp eq i64 %s7.next, 4
L395:   br i1 %s7.ec, label %sib7.exit, label %sib7.header
L396: 
L397: sib7.exit:
L398:   br label %sib8.header
L399: 
L400: sib8.header:
L401:   %s8 = phi i64 [ 0, %sib7.exit ], [ %s8.next, %sib8.header ]
L402:   %s8.next = add i64 %s8, 1
L403:   %s8.ec = icmp eq i64 %s8.next, 4
L404:   br i1 %s8.ec, label %sib8.exit, label %sib8.header
L405: 
L406: sib8.exit:
L407:   br label %sib9.header
L408: 
L409: sib9.header:
L410:   %s9 = phi i64 [ 0, %sib8.exit ], [ %s9.next, %sib9.header ]
L411:   %s9.next = add i64 %s9, 1
L412:   %s9.ec = icmp eq i64 %s9.next, 4
L413:   br i1 %s9.ec, label %sib9.exit, label %sib9.header
L414: 
L415: sib9.exit:
L416:   br label %top.latch
L417: 
L418: top.latch:
L419:   %t.next = add i64 %t, 1
L420:   %t.ec = icmp eq i64 %t.next, 4
L421:   br i1 %t.ec, label %exit, label %top.header
L422: 
L423: exit:
L424:   ret void
L425: }

4.1 Route the loop tree, not a flattened loop count

The outermost loop %top.header contains one candidate outer loop followed by nine sibling loops:

L289: top.header:
       ...
L291:   br label %pairX.outer.header

L293: pairX.outer.header:
       ...
L324:   br i1 %i.ec, label %pairX.exit, label %pairX.outer.header

       ...
L335:   br label %sib1.header
       ...
L409: sib9.header:
       ...
L416:   br label %top.latch
       ...
L421:   br i1 %t.ec, label %exit, label %top.header

LoopInterchangePass::run sees twelve loops in breadth-first order, but getNestDepth() reports depth three. The direct children of %top.header are the candidate outer loop and nine sibling loops, so isLinearLoopList routes this tree to the fallback. The old descendant-count depth check rejected it before reaching the candidate.

4.2 Carry depth and retain only a direct leaf pair

The worklist walks from the root to the candidate outer and inner loops:

L289: top.header:               (absolute depth 1)
L293: pairX.outer.header:       (absolute depth 2)
L298:   br label %pairX.inner
L300: pairX.inner:              (absolute depth 3, leaf loop)
       ...
L316:   br i1 %j.ec, label %pairX.outer.latch, label %pairX.inner

%pairX.outer.header has exactly one child, and %pairX.inner is a leaf. The carried depth lies within policy, so this adjacent pair is retained. None of the sibling loops belongs to this selected parent/leaf pair.

4.3 Apply canonical checks to the pair alone

The selected loops have canonical headers, backedges, and a single exiting block:

L293: pairX.outer.header:
       ...
L300: pairX.inner:
       ...
L316:   br i1 %j.ec, label %pairX.outer.latch, label %pairX.inner
L318: pairX.outer.latch:
       ...
L324:   br i1 %i.ec, label %pairX.exit, label %pairX.outer.header
L326: pairX.exit:

The fallback checks LoopSimplify form, computable trips, single backedges, and a single exiting block for each loop. An unrelated sibling can no longer fail a pair-local structural or ScalarEvolution check.

4.4 Keep ancestor dependence context while scoping memory locally

The dependence matrix uses the full three-loop chain:

ancestor column 0:  L289  %top.header
candidate column 1: L293  %pairX.outer.header
candidate column 2: L300  %pairX.inner

The memory inputs, however, come only from the candidate outer subtree:

L305:   %idxA = getelementptr inbounds [1335 x double], ptr %A, i64 %j, i64 %i
L306:   %a = load double, ptr %idxA, align 8
       ...
L311:   %idxC = getelementptr inbounds [1335 x double], ptr %C, i64 %j, i64 %i
L312:   %c = load double, ptr %idxC, align 8
L313:   %sumC.j.next = fadd reassoc double %sumC.j, %c

This separation is deliberate. DependenceAnalysis retains the enclosing %top.header direction, while memory collection stops at the candidate outer subtree. In this test case, that scope excludes the three stores at L332-L334. The nine sibling loops at L337-L416 hold no memory, and a sibling loop that did access memory would be excluded the same way because the interchange does not reorder sibling loops.

4.5 Reuse existing profitability on the explicit chain

The three accesses traverse arrays whose leading dimension is 1335:

L305:   %idxA = getelementptr inbounds [1335 x double], ptr %A, i64 %j, i64 %i
L308:   %idxB = getelementptr inbounds [1335 x double], ptr %B, i64 %j, i64 %i
L311:   %idxC = getelementptr inbounds [1335 x double], ptr %C, i64 %j, i64 %i

CacheCostManager receives %top.header, %pairX.outer.header, and %pairX.inner as one explicit chain. The existing model favors the swapped order for these accesses. Its reference groups come from the candidate leaf %pairX.inner, so the stores at L332-L334 and the sibling loops at L337-L416 stay outside the cost computation.

4.6 Transform the pair without moving its LoopInfo sibling slot

The candidate produces observable live-out reductions before control enters the first sibling:

L326: pairX.exit:
L327:   %sumA.live = phi double [ %sumA.i.lcssa, %pairX.outer.latch ]
L328:   %sumB.live = phi double [ %sumB.i.lcssa, %pairX.outer.latch ]
L329:   %sumC.live = phi double [ %sumC.i.lcssa, %pairX.outer.latch ]
       ...
L332:   store double %sumA.live, ptr %R, align 8
L333:   store double %sumB.live, ptr %rB, align 8
L334:   store double %sumC.live, ptr %rC, align 8
L335:   br label %sib1.header
L337: sib1.header:

The existing transform interchanges the loops at L293-L324. replaceChildLoopWith keeps the transformed pair before %sib1.header in the parent's LoopInfo child list. The live-outs at L326-L334 remain observable, and the existing transform retains responsibility for LCSSA, LoopInfo, DominatorTree, and ScalarEvolution updates.

4.7 Stop after one successful fallback transform

The fallback returns after transforming the selected region:

selected and transformed: L293-L324
preserved sibling region: L337-L416

It leaves the sibling region unchanged and does not enter the standard multi-swap path during the same invocation. This nest yields exactly one eligible candidate. Enumeration rejects %top.header because it has ten children, and it rejects each sibling loop at L337-L416 because a sibling loop contains no inner loop. The pass therefore transforms the only candidate and returns.

A nest with several eligible candidates stops the same way. The pairs left unattempted remain structurally valid because eligible pairs never overlap or nest. The fallback leaves them unattempted because one interchange per invocation is its stated scope. Section 3.4 describes the missing multi-pair policy.

5. Scope boundary

The patch deliberately does not distribute an outer epilogue. The original swim workload still needs loop distribution to isolate the later diagonal update and a runtime guard for a dynamically bounded enclosing loop.

The fallback does not handle a non-leaf selected child, multiple transforms in one invocation, unknown enclosing dependence context, or a candidate that fails existing canonical, legality, or profitability rules. These cases remain unchanged rather than receiving weaker proof.

The hidden -loop-interchange-enable-inner-subnest-fallback option is an operational escape hatch for fallback transformation, not a byte-for-byte behavioral revert. Corrected depth policy, direct non-linear dispatch, and diagnostic ownership remain active.

6. Maintenance invariants

The implementation relies on these invariants:

  1. Candidate memory scope and dependence-column scope are different. Memory comes from the reordered subtree. Columns cover every enclosing ancestor.
  2. Absolute DependenceAnalysis levels require an outermost root. Keep the root assertion and the carried-depth versus ancestor-chain assertion.
  3. Candidate ordering is deepest first, with stable breadth-first order among equal-depth ties. Keep one overflow marker if budget diagnostics remain observable.
  4. Expensive work stays behind both the attempt budget and memory-ratio prefilter. Do not replace those bounds with a descendant-count eligibility cliff.
  5. Cache analysis must consume the selected chain explicitly.
  6. Any nested-loop replacement must preserve the old sibling slot.
  7. Any extension to a non-leaf child must preserve the current leaf-pair contract for existing candidates.

7. Known gaps and uncertainties

  • TODO, valid but unhandled: deeper selected subnests and more than one fallback interchange per invocation need separate policy and profitability.
  • Asserted invariant: the fallback requires its LoopNest root at absolute depth one. A deeper root requires rejection or explicit level remapping.
  • Validated limitation: compile time was measured on the real 4,835-line scalar swim IR by toggling -loop-interchange-enable-inner-subnest-fallback on one binary. It covers fallback entry, enumeration, and memory-ratio rejection; three candidate attempts were rejected by the ratio guard. The delta is 0.005%, with a 95% confidence interval from -0.051% to 0.061%, on a RelWithDebInfo build with assertions and shared libraries. The switch-off mode keeps the depth policy, dispatch, and guard changes, so the comparison does not measure the whole patch or isolate legality, cache, and transform costs. Structural bounds and functional tests cover those paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment