Earlier, we taught the pass to find loops. Then we practiced constructing a metadata node in isolation. Today, we combine both by taking the loop we found, taking the metadata node we built, and actually staple one onto the other. Here, the pass changes the IR instead of merely observing it.
What I built: Commit 9dc0a65
What I understood:
As we can see in the IR, in br i1 %cmp, label %for.body, label %for.end, !llvm.loop !6, the !llvm.loop !6 at the end is metadata attached to a specific branch instruction at the latch block.
1) Latch Blocks and Branch Instructions:
- A latch block closes the loop. In our case, that's
for.inc, the last stop before the loop either repeats or ends. It contains the back-edge: the "go back to the top" jump.-
for.incincrementsi, checks whetheri < 10, and then either jumps back tofor.bodyor falls out tofor.end.
-
- Simply put, a latch is any block that has the loop header as one of its successors. Simple
forloops have exactly one, but complex loops with multiplebreakorcontinuepaths can have many, in which casegetLoopLatch()returns anullptr. - In memory, the branch instruction is a C++ object with fields for the condition being checked, where to jump if true/false, and a metadata slot.
- Right now, that metadata slot holds a pointer to the
mustprogressnode clang already put there. When we callBI->setMetadata("llvm.loop", LoopID), we're filling that slot with a pointer to our new combined node. Not replacing, but more like appending, because we cannot just overwrite the existingmust progressnode.
2) Getting the latch and its branch instruction:
- For every loop found, we first identify the latch block:
BasicBlock *Latch = L->getLoopLatch();
if (!Latch) {
outs() << " No single latch found, skipping.\n";
continue;
}
-
getLoopLatch()can return null if the loop has multiple latches, so we guard against that before proceeding. - Then we fetch the branch instruction at the end of the latch block:
BranchInst *BI = dyn_cast<BranchInst>(Latch->getTerminator());
if (!BI) {
outs() << " Latch terminator is not a branch, skipping.\n";
continue;
}
-
getTerminator()returns a generic instruction type, sodyn_cast<BranchInst>safely converts it. If it isn't a branch, we get null instead of a crash.
3) Building the hint tuple:
- Same as last time, we build the vectorize hint:
MDString *HintName = MDString::get(Ctx, "llvm.loop.vectorize.enable");
ConstantInt *TrueVal = ConstantInt::get(Type::getInt1Ty(Ctx), 1);
ValueAsMetadata *TrueMD = ValueAsMetadata::get(TrueVal);
MDNode *VectorizeHint = MDNode::get(Ctx, {HintName, TrueMD});
4) Reading what's already on the branch:
SmallVector<Metadata *, 4> MDs;
MDNode *ExistingMD = BI->getMetadata("llvm.loop");
-
MDsis just an empty list for now. -
BI->getMetadata("llvm.loop")asks the branch: "do you already have loop metadata?" In our case, clang putmustprogressthere, soExistingMDwill point to that. - We then loop through the existing node's operands and collect them:
if (ExistingMD) {
for (unsigned i = 1; i < ExistingMD->getNumOperands(); i++) {
MDs.push_back(ExistingMD->getOperand(i));
}
}
- We start at
i = 1because operand 0 is always the self-reference. We don't copy it. Our new node will have its own. - Then we push our new hint in:
MDs.push_back(VectorizeHint);
-
MDsnow has both:mustprogressand the vectorize hint.
5) Building the combined self-referential node:
- Same as last time, just with more content now:
MDNode *TempLoopID = MDNode::getTemporary(Ctx, {}).release();
SmallVector<Metadata *, 4> AllOps;
AllOps.push_back(TempLoopID);
AllOps.append(MDs.begin(), MDs.end());
MDNode *LoopID = MDNode::get(Ctx, AllOps);
TempLoopID->replaceAllUsesWith(LoopID);
MDNode::deleteTemporary(TempLoopID);
-
AllOpsis the complete list with itself,mustprogress, and the vectorize hint. The result:
!{ itself, mustprogress_hint, vectorize_hint }
6) Attaching it:
BI->setMetadata("llvm.loop", LoopID);
- This line updates the branch instruction's metadata slot to point at the new combined node. Everything before this was preparation. This is the moment the IR actually changes.
7) Informing the Pass Manager:
return PreservedAnalyses::none();
- Changed from
::all()to::none(), telling LLVM to recompute whatever it needs rather than trusting stale cached results. This is because we modified the IR.
Verifying it:
After rebuilding and running the pass, we check the output IR:
for.inc:
%inc = add nsw i32 %i.01, 1
%cmp = icmp slt i32 %inc, 10
br i1 %cmp, label %for.body, label %for.end, !llvm.loop !6
And at the bottom of the file:
!6 = distinct !{!6, !7, !8}
!7 = !{!"llvm.loop.mustprogress"}
!8 = !{!"llvm.loop.vectorize.enable", i1 true}
Before our pass ran, !6 had two operands. Now it has three. !8 is new. Our pass merged the existing hint with the new one rather than overwriting it.
Testing with the vectoriser:
Running both passes in sequence (ours first, then LLVM's built-in loop vectoriser) gives us this:
remark: <unknown>:0:0: loop not vectorized: call instruction cannot be vectorized
This is exactly the expected result. The vectoriser now explicitly considers the loop because of our hint. It simply declined because our loop calls printf, and you can't split a function call across SIMD lanes. The hint worked but the loop just isn't actually vectorisable with this test case.
To see vectorisation happen properly, we need a loop doing pure arithmetic on an array, with no function calls.
What's next: Testing the hint pass with a matrix multiplication loop.
Musings:
I don't think of myself as someone who procrastinates often, though perhaps everyone says that. I'm human too, and I postpone things more than I'd like. So it was of some solace to discover that even Marcus Aurelius lamented about this. Perhaps some struggles are timeless.
Time, though, is peculiar. We experience it as a relentless march forward, with every second disappearing forever. A clock seems to mock us with its ticking. And yet its very shape suggests something different. Its hands always return, holding the promise of "again." Time feels both linear and cyclical at once: always moving forward, yet endlessly repeating itself.
Whenever I find myself somewhere, whether familiar or new, I wonder what stood there a century ago. "What was it like here? Who lived here? What sights did they see?"
A hospital near my home became a shopping mall. A place where countless stories ended became a place where new ones began: weddings, birthdays, celebrations, and the like. The walls changed. But maybe what the place ultimately held never did. It remained a vessel for human lives, only in a different form.
Go back another hundred years and perhaps where I'm sitting was nothing but a forest. Maybe a bird once perched on the very spot where I now sit, looking at the trees, assuming the greenery was all forever. It's amusing to think about whether permanence is a uniquely human fantasy, or whether every living thing mistakes the world it inherits, for a world that will always exist.
The more I think about history, the less convinced I am that we are fundamentally different from those who came before. Did they look to the future with the same mixture of hope and confusion? Did they believe they were living through unprecedented change? Did they contemplate on their place in this world... on their role in this grand scheme? Every generation seems to ask the same questions, albeit through different lenses.
And that leaves me wondering about something stranger still. If everything changes, from cities, forests, and buildings, to people and memories... who or what remembers it all? Who keeps the ledger of existence? Is there anything that witnesses every beginning and every ending without itself being altered? To whom is time nothing? Who doesn't have to wonder what it's really meant for? What ultimately remains?
I hope to find answers one day. Tomorrow feels like a good day to start ;)





Top comments (0)