RFC: Enable ClangIR Build By Default-

Hi all-
As many of you know, a large number of folks have been working on ClangIR implementation over the last few years. More recently, this effort has been upstreamed and expanded (to the point that the Incubator was closed about 6 months ago) and has now hit a maturity point where we believe that it would be valuable to have this functionality by default.

The flag -DCLANG_ENABLE_CIR=Off is/would still be available for folks who wish to disable this.

Consequences of this:

1- MLIR is now brought in by default as a Clang dependency. This will increase the build time of the entire compiler by roughly 1/3 based on my non-scientific estimates.

2- Buildbots/runs of check-clang will now include CIR tests by default, including Continuous Integration. This would mean that committing a patch that breaks CIR would require ‘fixing’ ClangIR tests. The ClangIR maintainer team (myself, @bcardosolopes and @andykaylor amongst others) will definitely commit to help folks fix these tests, even if it includes providing a diff to fix it for them. This is actually a fairly rare situation, over the last few years we’ve only had changes break our tests ~12 times.

3- This does NOT change the default codegen behavior. ALL builds, unless specified with -fclangir

4- Until now, ClangIR hasn’t put anything in the release notes. This would require new patches to ClangIR to be in the release notes starting with the next release.

Why we think we’re ready:
ClangIR has hit a maturity point where we believe this is beneficial to the project as a whole, as a future direction, but also to the ClangIR project.

We believe it is sufficiently mature because:
1- Clang with -fclangir can successfully pass >98% of the SingleSource test suite, a number we expect to hit 100% in the very near future.
2- Clang with -fclangir can successfully pass 100% of the MultiSource test suite.
3- While a -fclangir stage2 build only gets through ~30% of a ninja build, we believe that there are only a small handful of issues with our ABI handling that will likely be fixed by the time this RFC would be approved, and will get us to a 100% self-build.
4- The libcxx test suite is now up over a 98% success rate with a -fclangir compiler.
5- Internal testing has shown that the Perennial Test Suite passes all suites at near parity (99%+) of the compiler without fclangir. Plum Hall testing is also roughly the same.

AS further background/success numbers, check out our August 2026 Progress Report:

In that build, self-hosting without counting Ninja dependencies shows a >99% self-hosting success rate, and most of SPEC2017. Note that most of the top items on that list have since been fixed, so those numbers are much improved, even from last week!

Why now?
More, “why in the next few months” at least, based on the RFC process :slight_smile:
1- We believe ClangIR has hit a point where it is actually USEFUL for many work loads. Enabling it by default would result in folks who use release binaries in some way to have a chance to try ClangIR out and give us feedback for future development!
2- ClangIR is starting to develop some optimization passes that have shown SIGNIFICANT improvements in certain workloads, despite being fairly ‘trivial’ to implement. We think that enabling this by default will encourage others to start contributing beneficial C/C++ optimizations to the compiler.
3- ClangIR provides an opportunity for Static Analysis in a way that Clang has not yet benefited from. We believe that enabling it by default will result in greater contributions from developers who wish to improve/implement these.
4- A few different contributing companies have shown interest in having ClangIR in their downstream projects. While they could enable it explicitly, we believe that this shows a general appetite
5- While the breakages have been rare, some have been quite painful, and we hope to get more notice/notification before it breaks all of our builds.

What about enabling fclangir by default?
This is very much NOT proposed for good reason: while ClangIR is quite mature as far as the amount of code it can successfully translate, it is missing quite a few important features (Modules, Debug Info, sanitizers, non-x86-target support, etc). While this is work we intend to progress on, we do not believe that ClangIR is nearly ready to be enabled by default in builds.

**ASSOCIATED PATCH: **

22 Likes

Do you have more information on these optimizations? I think I remember there being efforts around getting something like Representing Data Collections in an SSA Form | IEEE Conference Publication | IEEE Xplore in ClangIR, but it doesn’t seem like anything is upstream.

We’ll need to do some minor changes to LLVM premerge after this that I can handle. Given we’re already building lots of stuff anyways, I don’t think the overall time increase will be too noticeable.

2 Likes

1/3 build time increase sounds like a significant impact. Is it possible to introduce more PCH for MLIR to mitigate the impact?

1 Like

At least one of the authors of that paper has been involved in the development of CIR. I also haven’t seen any upstream analysis as complex as what that paper describes, though I think it’s within the scope of what’s possible and I’d love to see more academic research of the optimization opportunities CIR enables.

That said, when I read things like “we perform dead element elimination…” it always makes me think that a change like that is better performed at the source level. Changing data structures in the compiler requires heroic (and usually fragile) efforts, but teaching the compiler to say things like “Hey, I noticed you aren’t using this field in your struct…” is much more practical. I think CIR is a perfect platform for that sort of analysis, and even the next level (which the MEMOIR paper is chasing) of “It looks like this would be faster if you rearranging these data structures in the following way…”.

Bringing this back around to the RFC topic, I think enabling CIR in Clang builds by default moves us towards generating interest in developing areas like that.

3 Likes

-1.

I primarily build Clang because I desire a C/C++ front-end for LLVM-IR and I usually don’t build MLIR. If I compare the costs of building Clang with CIR (much longer build times, higher max-rss, larger build dirs) with the possible benefits I see for myself in potentially using ClangIR, also in the future (~none), I am forced to conclude that this is a bad tradeoff. I haven’t conducted a user survey, but I feel that I’m not the only one who has a similar cost-benefit ratio.

On c-t-t, this is a +130% increase, not +33%, so this more than doubles the time to build Clang. I also just ran an experiment locally (dylib+pch build) to confirm the results:

main main+CIR increase
CMake wall time 22.01 33.26 +51.1%
Build CPU time 8004.51 19010.74 +137.5%
Build wall time 113.20 262.03 +131.5%
max-rss 948304k 2935520k +209.6%
build dir size 2.0G 4.0G +100.4%

Given the continuous efforts to make Clang/LLVM build by default to use less resources and to be more accessible to people without as powerful hardware as the machine I did these measurements on, this seems to be a large step in the wrong direction.


I think it would be good for ClangIR to be more visible and get more test coverage. I don’t, however, understand, why this has to be integrated in the clang/ project or be enabled through -fclangir. Maybe we could have a new top-level project with a separate driver (clang-cir) that leaves the existing Clang binary alone for users that don’t want/need it?

11 Likes

Some detail on the optimization side, since I have been working on this part of the CIR STL recognition.

The idea is standard library algorithm recognition. At CIR a call like std::find is still a single operation carrying its identity, before inlining flattens it into a scalar loop in LLVM IR. A CIR pass reads that identity, proves from the source types that the rewrite is sound, and lowers the call to the matching C library routine such as memchr. The same approach extends to other STL algorithms.

It is landing as a small PR series so each piece is reviewable:

4 Likes

Does this separate driver mean just a driver like flang, or a standalone executable like llvm-bolt? For LLVM_TOOL_LLVM_DRIVER_BUILD users, everything should be integrated together.

While I see the benefit of moving to the ClangIR approach, I’m not enthusiastic about having a new mandatory dependency for building clang. Succinctly, I’d like to know if there is anything that could be done about the overhead of building MLIR. In my experience MLIR is the second most expensive project to build behind Flang, so I’d like to see some comprehensive research into how much overhead this adds and how much can be improved in MLIR. From my experience, MLIR is a small set of very, very large files that take a long time to build on less powerful machines.

It may also make sense to try to minimize the dependencies so that only the necessary dialects needed for CIR are built and linked from MLIR. I’m assuming we would also need to inherit flang’s trick of auto-appending MLIR because most people will not think it is a dependency.

6 Likes

We don’t have anything like that one, no. We have done a few simple ones with an Intern (@Rithik ) who might be able to share, but it is admittedly only a few months of work. We got some pretty strong benefits from ‘simple’ opts that LLVM doesn’t do (replacing std::find with memchr in some cases, some standard library function recognition passes, and some simple loop-interchange stuff).

That said: I suspect our ‘low hanging fruit’ here is definitely going to be our recognize the standard library and do stuff that you can only know if you know what the standard says direction. For example. Given (a pretty good example of, but horrifyingly made up on the spot):

void collect_chars(std::vector<char> &myVec, const std::string & src) {
for (auto c : src)
  myVec.push_back(c);
}

We could gain significant performance gain by minimizing allocations by translating this to effectively:

void collect_chars(std::vector<char> &myVec, const std::string & src) {
myVec.reserve(myVec.size() + src.size());
for (auto c : src)
  myVec.push_back(c);
}

This is a reasonably common pattern (that is, looping through… something and push_back’ing to it) that can often benefit from the reserve. However, to do this you have to ‘know’ that the standard library provides a size for each collection, what those do, what reserve is, and the effects of push_back.

An even smarter opt would be to replace that all with a reserve + a memcpy + size-update (so):

void collect_chars(std::vector<char> &myVec, const std::string & src) {
  myVec.reserve(myVec.size() + src.size());
  memcpy((void*)&*std::next(myVec.begin(), myVec.size()), &*src.begin(), src.size());
  myVec._M_impl._M_finish += src.size();
}

That is obviously a breaking transformation UNLESS you know enough about the standards behavior (and implementations of!) enough to make that transformation. But that is the sort of thing we’d love to do someday, but we believe ClangIR enables it.

THIS I personally don’t have an idea about? Hopefully someone else responding can let me know, but would presume that others have optimized the MLIR build, since MLIR is built pretty often. But you’ve encouraged me to collect ‘better’ numbers on this. MY Results are pretty unscientific since it is a shared machine, but it should be pretty representative?

ClangIR results: 9553 Ninja Targets

CXX=`which clang++` CC=`which clang` cmake -G Ninja ../llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;mlir" -DCMAKE_INSTALL_PREFIX=install -DCLANG_ENABLE_CIR=On -DLLVM_OPTIMIZED_TABLEGEN=true -DLLVM_ENABLE_RUNTIMES=compiler-rt -DLLVM_USE_LINKER=lld && time ninja

<snip>
real    15m6.032s
user    1188m43.467s
sys     65m11.077s

WITHOUT ClangIR (but with MLIR): 9412 Ninja Targets

CXX=`which clang++` CC=`which clang` cmake -G Ninja ../llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;mlir" -DCMAKE_INSTALL_PREFIX=install -DCLANG_ENABLE_CIR=Off -DLLVM_OPTIMIZED_TABLEGEN=true -DLLVM_ENABLE_RUNTIMES=compiler-rt -DLLVM_USE_LINKER=lld && time ninja

<snip>
real    14m22.347s
user    1148m7.760s
sys     62m43.625s

Without MLIR either: ~6689 Ninja Targets

CXX=`which clang++` CC=`which clang` cmake -G Ninja ../llvm -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra" -DCMAKE_INSTALL_PREFIX=install -DCLANG_ENABLE_CIR=Off -DLLVM_OPTIMIZED_TABLEGEN=true -DLLVM_ENABLE_RUNTIMES=compiler-rt -DLLVM_USE_LINKER=lld && time ninja

<snip>

real    8m16.369s
user    682m7.221s
sys     39m27.949s


So @aengelke 's findings of closer to double is seemingly accurate. This is unfortunate, but a necessary cost of using MLIR I believe.

In my experiment real is 1.83x, user is 1.74x, and sys is 1.65x the time.

The long term goal is to make this the ‘default’ backend for Clang. While we are many years from being able to disable/remove the direct-to-LLVM build, this is something you’d have to deal with eventually?

That said, in the meantime -DCLANG_ENABLE_CIR=Offwill be available for the forseeable future.

2 Likes

At the moment my patch is doing Flang’s approach of auto-appending MLIR. As far as including only PARTS of MLIR, it currently doesn’t seem to support anything like that? But picking/choosing dialects or something COULD perhaps be possible? @mehdi_amini : do you have any feedback/options we could perhaps include to reduce the cost?

I meant a separate top-level project building a separate executable. I don’t care about exotic builds like driver builds.

FWIW, here’s my config, I don’t build clang-tools-extra (I never use that) and only the native target:

time CC=clang CXX=clang++ cmake -DLLVM_ENABLE_PROJECTS="llvm;clang" -DLLVM_TARGETS_TO_BUILD="X86" -DCMAKE_BUILD_TYPE=Release -G Ninja -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_LINK_LLVM_DYLIB=ON -DLLVM_USE_LINKER=lld -B llvm-build-tmp30 -S llvm-project/llvm && /usr/bin/time ninja -C llvm-build-tmp30
time CC=clang CXX=clang++ cmake -DLLVM_ENABLE_PROJECTS="llvm;clang;mlir" -DLLVM_TARGETS_TO_BUILD="X86" -DCMAKE_BUILD_TYPE=Release -G Ninja -DLLVM_ENABLE_ASSERTIONS=ON -DLLVM_LINK_LLVM_DYLIB=ON -DLLVM_USE_LINKER=lld -B llvm-build-tmp31 -S llvm-project/llvm -DCLANG_ENABLE_CIR=ON &&  && /usr/bin/time ninja -C llvm-build-tmp31 -k0
FAILED: bin/cir-opt
ld.lld: error: undefined symbol: cir::test::registerTestCIRAliasAnalysisPass()
>>> referenced by cir-opt.cpp
>>>               tools/clang/tools/cir-opt/CMakeFiles/cir-opt.dir/cir-opt.cpp.o:(main)

NB: cir-opt failed to link. clang++ is a ThinLTO+PGO Clang 23, llvm-project f4ad12c3e43, machine has 48c/96t.

I don’t know the latest compile-time numbers of ClangIR, but from what I’ve seen, the increase can be substantial. Given that for typical C++ programs, even with -O3, the Clang front-end tends to dominate compile times (and also gets slower over time) already (e.g., an LLVM Release build with PCH spends ~55% of the time there), you can consider me opposed to any larger increase in compile times (both in -O0g and -O2g) and any non-large-but-significant increase without a corresponding value proposition. You can also consider me to be strongly opposed to removing the current not-fast-but-ok CodeGen as long as I have to regularly compile large C++ code bases.

I understand where you’re coming from, but I think the user base of Clang is much larger than the user base that benefits from ClangIR being the default. I haven’t conducted a user survey, though, this is just a skewed sample from people I regularly talk to and work with.

@makslevental at some point started looking at using PCH for MLIRIR, but at least the initial version didn’t seem to improve things. I don’t know any more details, though.


While we’re talking about the transformations possible in ClangIR, I can also expand on why I don’t expect ClangIR will be providing value to me.

This argument would be much stronger, IMHO, if it didn’t only apply to the standard library: most larger C++ projects (LLVM, WebKit, …) have their own substitutes for many things, because the C++ stdlib data structures are largely bad. I don’t see LLVM replacing llvm::SmallVector with std::vector, but it would be nice if repeated size checks could be removed and reserves added to llvm::SmallVector, too. That would require, however, more possibilities of expressing invariants and operations and I’m not aware of any work or plans in this direction.

Are these things that LLVM could do but didn’t or things that LLVM can’t do without much effort? For things implementable at the IR level, I think there would be a larger value in implementing them there so that also other front-ends benefit. E.g. loop->memchr is also something I’ve also noticed in some Rust code I’ve been working on and that seems to be doable as IR transformation.

I’m yet to see convincing and substantial optimization cases in a code base like LLVM that ClangIR would enable and would be unreasonably difficult to achieve otherwise.

THIS is something we’re actually not seeing much of an overhead to. @bcardosolopes is running it internally, and the compile time increases are VERY minimal based on his report. At the moment we’re not discussing enabling -fclangir by default, so this RFC would result in a +0%. We wouldn’t propose -fclangir by default (that is, enabled at runtime) until we get a signficant performance and diagnostic improvement.

AN additional thing: some of our more difficult analysis steps in Clang could be reduced (basically any of our CFG stuff) and replaced with CIR passes. This is again on a long-term roadmap.

TBH, I forsee a future where OTHER libraries would be given a way to opt-into an acknowledgement that your type is ‘vector-like-enough’ or ‘collection-enough’ to enable similar optimizations. You’re right that no current effort exists to do this, but it is definitely on a long-term-roadmap. We HAVE seen a couple of folks in recent times wish to express this sort of thing on types already to opt into existing non-ClangIR opts.

The terse answer here is: @Rithik was able to produce significant gains on libcxx benchmarks/other workloads with very minor effort. LLVM optimizations for these sorts of things are perhaps technically possible (showing otherwise would basically disprove Chris Latner’s LLVM PhD), but the difference is that the higher level of abstraction/greater permissions that we give to ClangIR to ‘know’ the standard library, means these become easier to express. Our representation of loops has already resulted in one of the loop interchange opts that Rithik looked into going from “this is too hard, leave as a TODO” in LLVM, to something he was able to do in a few hours.

In reality: this is the sales-pitch of MLIR: We give more information to make these optimizations easier to prove out. OTHER dialects have seen a ton of benefit from these sorts of higher level abstractions (not MLIR, but Rust of course is a great example), so to me it seems like a bit of a ‘given’.

It’s probably doable if someone wants to wire it up in LoopIdiomRecognize, but it’s likely going to be a lot more fragile (e.g., relies on inlining and loops being in certain forms. LoopIdiomRecognize still can’t recognize non-rotated loops). I imagine it will be cleaner if you can do it in the frontend on a higher-level IR, but haven’t thought too hard about the tradeoffs. No language independence for the optimization would be a bit of a bummer.

What’s the advantage of building it by default vs just having it enabled in the pre-commit CI builds? This seems like it will create a worse experience for new users who may not know about the toggle flag.

Also, the build time increases measured by @aengelke seem pretty bad. Has anyone looked into why this is slow or how to improve this?

3 Likes

The advantage is getting it out into distros/etc as well. Just enabling it in Pre-commit doesn’t make it usable/useful to all our users. The intent here is to move this project closer towards being the replacement of our current codegen, so this is a step we have to make eventually. We believe this is something that is nearing generally ‘useful’ to a significant part of our userbase.

They are, and it is unfortunate. The ‘why’ is really “we are including all of MLIR”. If you look at my numbers above (the middle group is JUST +MLIR, no CIR involved). I’m evaluating how we can possibly reduce how much MLIR we include (I suspect there are some dialects we can skip out on?), but I’m hopeful others more involved in MLIR can help out with that too.

1 Like

@aengelke Were you building with ninja or ninja clang ?

1 Like

Thanks for proposing this Erich, way to go!

On that, I’ll back Erich’s offer concretely: if your patch breaks CIR tests and you don’t want to deal with it, happy to help!

One anecdote: we (cc @ivanmurashko) got 3-5x total time speed bump when using a CIR-based bugprone-use-after-move against a good fraction of our codebase. This was done by making clang-tidy CIR-aware and re-implementing said check on top of CIR.

@tommymcm (one of the authors) worked with us last summer on some of those optimizations. At the time ClangIR wasn’t yet able to build big projects and make them fully functional but we got good direction out of his early experiments. The picture is different now in our downstream compiler (based on ClangIR upstream), we’ve been building real C++ (clang itself, webkit, etc) and Android applications (social apps) with -fclangir and actually running them, and we are also currently experimenting with idiomatic optimizations (like hinted by Andy/Erich/Rithik and others in this thread), hopefully we’ll be able to share and upstream more of that soon.

The build time cost is real, and I think it’s a fair concern. Here’s some extra data from a modern machine perspective:

config -j ninja build edges ninja clang CPU (u+s) peak RSS size (build dir)
off 16 3,440 752.6 s 11,743 s 2.66 GB 5 GB
on 16 5,574 1315.3 s 20,130 s 6.16 GB 6 GB
off 48 3,440 299.4 s 13,363 s 2.66 GB 5 GB
on 48 5,574 551.9 s 21,651 s 6.16 GB 6 GB
off 96 3,440 188.6 s 14,213 s 2.65 GB 5 GB
on 96 5,574 383.0 s 23,614 s 6.13 GB 6 GB
  • One run per data point. CPU (u+s) is stable and the three -j
    points agree, but the wall-clock figures carry unquantified noise.
  • X86;AArch64, not all targets. A smaller LLVM makes MLIR a larger share
    of the total, so this overstates the relative cost versus a default
    all-targets build. Conservative direction, but worth stating.

How much of this was done by you and how much of this was done by an LLM? The language here seems very LLM-speak and distinct from the rest of your message.

Yea, only the table and the small text below it was generated by an LLM, I didn’t bother (nor saw any good reason for) fixing the paste for it.

So I think the IMPORTANT thing from your chart is the difference in ‘edges’. The ‘edges’ are expensive, particularly in MLIR, and your above numbers show ~+2134. My numbers are similar above (2864, but I’m also doing clang-tools-extra in my build, which has some additional linker steps I think?).

I’m going to work on this more next week, but I have been siccing claude on trying to see if we can create a ‘standalone’ version of MLIR, which would avoid compiling unnecessary dialects (like, why compile FIR for clang?) and unnecessary MLIR tooling. I am pretty optimistic I can take a significant hunk out of it based on its feedback, but it is going to require a bit of work. I intend to work on that to minimize the pain here.

1 Like