DEV Community

Lahari Tenneti
Lahari Tenneti

Posted on

LLVM #11 — Attaching Metadata to Loops

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.inc increments i, checks whether i < 10, and then either jumps back to for.body or falls out to for.end.
  • Simply put, a latch is any block that has the loop header as one of its successors. Simple for loops have exactly one, but complex loops with multiple break or continue paths can have many, in which case getLoopLatch() returns a nullptr.
  • 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 mustprogress node clang already put there. When we call BI->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 existing must progress node.

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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode
  • getTerminator() returns a generic instruction type, so dyn_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});
Enter fullscreen mode Exit fullscreen mode

4) Reading what's already on the branch:

SmallVector<Metadata *, 4> MDs;
MDNode *ExistingMD = BI->getMetadata("llvm.loop");
Enter fullscreen mode Exit fullscreen mode
  • MDs is just an empty list for now.
  • BI->getMetadata("llvm.loop") asks the branch: "do you already have loop metadata?" In our case, clang put mustprogress there, so ExistingMD will 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));
    }
}
Enter fullscreen mode Exit fullscreen mode
  • We start at i = 1 because 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);
Enter fullscreen mode Exit fullscreen mode
  • MDs now has both: mustprogress and 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);
Enter fullscreen mode Exit fullscreen mode
  • AllOps is the complete list with itself, mustprogress, and the vectorize hint. The result:
!{ itself, mustprogress_hint, vectorize_hint }
Enter fullscreen mode Exit fullscreen mode

6) Attaching it:

BI->setMetadata("llvm.loop", LoopID);
Enter fullscreen mode Exit fullscreen mode
  • 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();
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

And at the bottom of the file:

!6 = distinct !{!6, !7, !8}
!7 = !{!"llvm.loop.mustprogress"}
!8 = !{!"llvm.loop.vectorize.enable", i1 true}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)