A general-purpose loop iteration-space splitting utility (LoopSplitUtils)

Hi all,

We keep running into a gap in LLVM: there is no fair reusable mechanism to split a single counted loop’s iteration space into N caller-chosen contiguous partitions, with correct guards and live-out/reduction reconstruction. The existing splitters are all policy passes: they discover their own opportunities from in-body patterns and decide when to fire, so a caller can’t simply say “split this loop into the partitions I choose.” Today any transform that needs that re-implements a narrow, special-cased version of it.

The partition boundaries often come from outside the loop body (profile-guided hot-range isolation, a higher-level transform that already knows the ranges, language/region information), where there is no single in-body condition for a pass to pattern-match.

As a quick illustration, an N-way partition makes IV-dependent conditions loop-invariant per partition so later passes drop them:

// Three IV-dependent regions: needs partitioning at K1 AND K2 (N = 3 sub-loops).

for (int i = 0; i < n; i++) {

if (i < K1) a[i] = f(i);

else if (i < K2) a[i] = g(i);

else a[i] = h(i);

}

Prior art. This optimization, “index set splitting” (a.k.a. loop index splitting), is well established: GCC implements it as -fsplit-loops (tree-ssa-loop-split), and LLVM itself once had a dedicated LoopIndexSplit pass (still credited in CREDITS.TXT), removed in 2010 as “neither maintained nor used by anyone.” Since then the only in-tree form is IRCE’s range-check-specific, 3-way LoopConstrainer. LoopSplitUtils restores the general capability, but as a reusable, N-way, caller-driven utility.

What already exists (and why none is a general splitter):

  • peelLoop: peels a fixed number of iterations off the front/back as straight-line copies. Not sub-loops, not arbitrary partitions.
  • LoopConstrainer (used by IRCE): the closest thing; clones a loop into pre/main/post. But it’s hard-wired to 3 segments, the ranges are chosen internally for range-check elimination, and there’s no public N-way / caller-driven API.
  • LoopBoundSplit: splits into 2 loops at a single IV-dependent condition it discovers itself (diamond CFG, fixed preconditions); a policy pass, not a caller-specified, N-way mechanism.

The gap: none of these lets a caller say “split this loop into these N partitions” and get back correct, guarded sub-loops. So passes that want index-set splitting either bolt it onto an unrelated utility or duplicate the cloning + guard + SSA-rebuild logic.

Proposal: a standalone LoopSplitUtils that:

  • takes a counted loop plus an explicit, ascending list of contiguous partitions (inclusive [Start, End] ranges added via addPartition),
  • emits one guarded sub-loop per partition (empty partitions skipped, leading/middle/trailing handled uniformly),
  • clamps each partition to the true trip count (bottom-test safe), and
  • rebuilds reductions/live-outs across partitions via SSAUpdater.

Intended consumer. This follows the direction laid out in the EuroLLVM 2024 quick talk “Loop Iteration Space Splitting” (slides): a test pass that specializes IV-dependent conditions / isolates iteration ranges drives the utility with its own partition list. The test driver (-loop-split-points) exercises the same path in lit tests.

The pattern already lives in the tree several times over, each welded to its own pass. Instead of adding another copy, we’d like to land one reusable splitter that any pass can call.

If the community would rather extend one of the existing splitters (`LoopConstrainer`, `LoopBoundSplit`, …) into this general mechanism, we are open to doing that. Our preference, though, is to land `LoopSplitUtils` as a separate utility first — so the N-way, caller-driven interface and its test coverage can be reviewed on their own — and then converge the existing implementations onto it one by one (re-pointing each such pass at the shared utility and removing its duplicated cloning/guard/SSA-rebuild logic). That keeps each step small and independently reviewable and avoids destabilizing IRCE or `LoopBoundSplit` while the common mechanism is still settling.

Initial patch: [Transforms][Utils] Add LoopSplitUtils for iteration-space loop splitting by nema-ashutosh · Pull Request #205995 · llvm/llvm-project · GitHub

Happy to iterate on the interface and the limitations based on review. We have kept the first patch simple just focusing on trivial loops. If the direction looks right, we’ll follow this with the incremental update & migration outlined above.

Thanks!

Ashutosh

2 Likes

This is fine in terms of upstreaming for review, but I expect to see the full patch stack including migrations for LoopBoundSplit and LoopConstrainer before anything lands. Otherwise it is not obvious that the implemented utility is really a reusable abstraction, and does not just introduce the N+1st implementation.

Thanks @nikic, that makes sense. We’ll share the full migration plan with patch stack shortly so you can confirm the utility is a genuinely reusable abstraction. In the meantime, it would be great if reviewers could start on the already-posted LoopSplitUtils patch, since the migrations build directly on that interface.

Following up on the migration plan I mentioned earlier, I’ve opened a draft PR that shows the complete patch stack: [Transforms][Utils] Add LoopSplitUtils, a reusable iteration-space loop splitter by nema-ashutosh · Pull Request #209142 · llvm/llvm-project · GitHub .

Just to set expectations, this PR is intended just for demonstration of Loop split utility and how it can be used to migrate both IRCE and LoopBoundSplit. The goal is to make the approach concrete, show how LoopSplitUtils is expected to be used, and help evaluate whether it stands on its own as a reusable abstraction.

As discussed previously, the migration patches build directly on top of the LoopSplitUtils API. For that reason, feedback on the already-posted utility patch would be especially valuable and would help unblock the rest of the series.