Wednesday, October 14, 2026

Lauterbach and JetBrains Announce TRACE32 Integration in CLion IDEHoehenkirchen, Germany/Amsterdam, Netherlands — October 14, 2026 — Lauterbach and JetBrains have integrated TRACE32® debugging tools with CLion, JetBrains’ professional C and C++ tool. The integration enables embedded developers to debug applications on target hardware directly from CLion, bringing code editing, navigation, and hardware debugging into one development workflow. Using the Debug Adapter Protocol (DAP), […]📝CLion : A Cross-Platform IDE for C and C++ | The JetBrains Blog

If this page is useful, please consider donating a coffee

Tuesday, October 13, 2026

Benchmarking any2bazel - towards more efficient migrationsBenchmarking any2bazel - towards more efficient migrations We’re fond of Bazel, and particularly fond of the capabilities it offers developers. Faster builds, made more efficient through remote execution and caching, made more understandable through declarative configuration, queryability, and precise logging – these deliver benefits and solutions to developers once Bazel’s all set up and integrated into their daily processes. But it’s a rare developer who enjoys spending their time and effort in build system configuration. Wouldn’t we rather be solving our customers’ problems? And the decision to migrate from an existing build system to Bazel is one that demands consideration of huge trade-offs; the labor and opportunity costs of engineering time spent on migration and adapting internal workflows to a new build system discourage the pursuit of its advantages. In early 2026 I became interested in AI-guided Bazel migration because the capabilities of models like Opus 4.5 and agentic tools like Claude Code seemed to offer a way of realizing those benefits without so much toil – that is, if AI-guided Bazel migration worked. I believed it would work eventually, if not at that point in time, given the rapid pace of capability growth in AI models and their harnesses. I also believed it would be possible to distill lessons learned from open-source projects’ Bazel migrations and AI-guided migration attempts into a skill. If done right, such a skill could elevate those capabilities, and the process of improving the skill with better distillation from more and more migration attempts could augment the "natural" capability growth coming from the models getting smarter. I was particularly struck by the question, "how would we know when we arrived?" It seemed answerable, if only we had an experimental laboratory that evaluated AI-guided Bazel migration attempts. Many months later, around the time any2bazel launched, I began work on any2bazel-lab , to bring experimental results to bear on the question of how well current AI models performed at automated Bazel migration, both with and without the any2bazel skill. Anthropic’s frontier models – a mix of Opus 5, 5.5, Fable 5.1, and a bit of Haiku 4.5 – extensively contributed to the lab’s development, and coordinated the execution of its migration experiments. Our experiments are ongoing. Here’s a summary of what we’re learning, and how.📝EngFlow Blog

Monday, October 12, 2026

GSoC 2026: Improving litThis summer I worked on improving lit, the LLVM Integrated Tester, as part of Google Summer of Code 2026. I got interested in compilers after seeing how much heavy lifting happens underneath a single line of code, especially on parallel accelerators like GPUs.Two courses at IIT Bombay, SysML and Advanced Computer Architecture, turned that from a passing interest into something I actually wanted to work on.I’d like to eventually move toward compilers or microarchitecture for parallel compute engines as a career, and LLVM felt like the right place to start: it’s the compiler infrastructure behind a large share of the toolchains people actually use, so performance work here doesn’t stay contained to one project.lit specifically appealed to me because the problems in it turned out to be systems problems, process scheduling, IPC, syscall overhead, GIL contention, not surface-level scripting fixes, which is exactly the kind of low-level work I wanted hands-on experience with before jumping into compiler internals directly. My mentors were Aiden Grossman , Paul Kirth , and Petr Hosek . The Project lit is LLVM’s test runner. RUN: lines look like shell commands, but lit’s own internal shell parses and runs them itself.A 2024 RFC made that the default everywhere, arguing it’s faster and more consistent across platforms than spawning an actual shell process for every line ( RFC ).The option to spawn a shell process instead still exists in the code, but now raises an error unless explicitly forced, and is on track for removal.By the time I started this project, it was lit’s own Python code, not a spawned shell process, parsing and executing every RUN: line, in every subproject in the monorepo, under ninja check-* (lit also runs a smaller set of tests in GoogleTest’s format, which skip RUN: entirely).None of that, parsing and executing RUN: lines, compiles or checks anything on its own.It’s the overhead sitting between a developer pushing a change and finding out whether it passed.lit hadn’t gotten much dedicated performance work before this project, even though every second it spends is a second not spent running an actual test.It’s testing infrastructure, and testing infrastructure tends to get left alone once it works well enough. Petr Hosek ’s project idea flagged three areas as worth a closer look: Excessive process creation : Builtin shell utilities like cat , diff , and resource-limit wrappers were spawning fresh Python interpreter subprocesses for every single invocation line. Execution engine & IPC bottlenecks : Locking and queueing overhead in the execution engine ( multiprocessing.Pool ), causing scaling limits on modern high core count systems. Legacy codebase idioms : Python 2 patterns, redundant string concatenations, and sub-optimal loop structures remaining across lit’s execution paths. Before writing the GSoC proposal, I ran lit under a debugger to see how testing actually happened underneath: where it spent its time, which paths were hot, and which of those hot paths had obviously unoptimized code sitting in them.That debugging pass is what the proposal’s four parts below are built from: Modernizing the Python implementation : cleaning up legacy Python 2 idioms and picking up things like @dataclass(slots=True) and functools.cache on lit’s hot paths. Reducing process overhead : moving the execution engine off multiprocessing.Pool and onto asyncio . Improving internal shell utilities : moving cat and diff in-process, and fixing a pipeline bug in env . Performance tuning via profiling : using Scalene to catch whatever the first three parts didn’t. The sections below go through what actually happened once those turned into pull requests over the summer, which was not always what the proposal predicted. The figure above shows how these fit together.Profiling (part 4) sits at the top because it feeds the other three tracks: Python modernization (part 1), reduced process overhead (part 2) and shell utility correctness (part 3).All four aim at one goal, making lit faster without changing test verdicts.The sections below are organized by these same four areas.I also wrote a small script around hyperfine early on, and later added a peak-RSS measurement pass to it.Most of the timing numbers below come from it. Modernizing the Codebase The first two weeks focused on eliminating legacy Python 2 idioms remaining in lit’s codebase, found by reading through every file in lit/ end to end: A suffix map implemented as a plain dict was replaced with an IntEnum .The dict mapped a fixed set of parser kinds to values with no structure tying them together.An enum names each member, gives it one definition site, and makes membership and iteration explicit.This was an idiom change and I did not measure a speed difference. zip(range(len(x)), x) loops were refactored to enumerate(x) .Both yield (index, item) pairs, but enumerate says so directly and drops the separate range and len calls.Again an idiom change with no measured difference. Explicit argument calls like super(Class, self) were updated to modern super() .The two-argument form repeats the class name, so renaming the class silently breaks it.The Python 3 form has no such coupling. String concatenation in the shell lexer’s ( ShLexer ) hottest method was changed from repeated += string building to list joins.This one was a performance fix, covered below. ShLexer was one of the hot paths that my initial profiling turned up: it runs on every character of every RUN: line, across every test file in the suite, and it built each token with str += inside a character loop continually reallocating intermediate strings, worst-case O(N²) on long tokens.That made its string-construction method worth optimizing and benchmarking directly. It reduced the CodeGen/X86 test suite execution time from 99.31s to 94.96s (4.35s, a 4.4% improvement) and the Transforms suite from 51.46s to 50.89s (0.57s, a 1.1% improvement). The proposal also planned to put @dataclass(slots=True) on lit’s hot data objects ( ShellCommandResult , ShellEnvironment , DiffFlags ).These get created once per test, and a slotted class drops the per-instance __dict__ , which cuts memory and speeds up attribute access.That part did not go according to the plan because slots=True on @dataclass needs Python 3.10, and the minimum Python version LLVM supports is 3.8.I used plain __slots__ instead, which works on 3.8 and gets the same benefit, and left a # Replace __slots__ with @dataclass(slots=True) comment in the code pointing at GitHub issue #200531 , filed so the switch happens once the minimum version is raised.This kept coming up over the summer: the 3.8 minimum ruled out several other modern constructs too, and each time the resolution was the same, ship the best version 3.8 supports with a code comment naming what should replace it once the minimum moves. Two other part 1 items were nice-to-have cleanups that didn’t ship early: replacing the hand-rolled _caching_re_compile with functools.lru_cache , and switching discovery.py from os.listdir() plus isdir() to os.scandir() .I didn’t have numbers showing either one would matter yet, so I put time into the changes above instead, and both only came back months later, once Scalene profiling near the end of the summer gave me the numbers to justify them.They’re under Future Work below. Pull Requests optimize ShLexer string construction modernize ParserKind implementation using Python3 IntEnum replace zip(range(len(x)), x) with enumerate(x) in ProgressBar refactor super() calls in ResultCode add __slots__ to hot shell execution objects handle config loading safely make MetricValue a proper abstract base class remove redundant f.close() in TestingConfig Moving lit to ProcessPoolExecutor At the start of the project, lit dispatched tests through multiprocessing.Pool and collected results in a loop calling ar.get(timeout) . That collection loop contained two issues: The timeout was calculated once before entering the loop, meaning --timeout did not anchor to a single absolute deadline as intended. Test results were matched back to tests by list index rather than by identity, meaning out-of-order task completions could attribute results to the wrong test. Migrating to concurrent.futures.ProcessPoolExecutor fixed both, through as_completed() and an explicit mapping from futures to tests.For the first issue, as_completed() takes the deadline once and yields each future as it finishes.The remaining time is then always measured against a single absolute point in time, so --timeout means what it says.For the second issue, each future is a handle to exactly one submitted test.A future: test dict therefore returns the right test whichever order tasks finish in, where the old code relied on list position.It also decoupled lit’s execution engine from the rest of the runner.That’s what let me swap in different backends later without touching anything else, more on that further down. Part 2 of the proposal aimed to reduce process overhead in the execution engine, which came from two costs paid for every test.The first is pickling: the main process serializes each Test object to send it to a worker over a pipe, and the worker serializes the result to send it back, so every test crosses the process boundary twice.The second is process creation: running a test means forking and exec-ing a fresh process for the tools in its RUN: lines, and that cost is paid again for every test.The proposal’s plan was to attack both by changing the execution engine.It sketched this migration as wrapping the whole thing in asyncio from the start, an async def _execute_async coroutine awaiting asyncio.as_completed() inside asyncio.run() .What actually shipped left asyncio out entirely: a synchronous loop around plain concurrent.futures.as_completed() , since that alone fixed both bugs. asyncio did come back later, but as a completely separate experimental engine rather than this migration, covered further down. The initial PR landed as a neutral refactor.Nine days later it got reverted, it had triggered two bugs in CPython’s concurrent.futures.process that multiprocessing.Pool had never hit. First, Douglas Yung reported an AArch64 macOS bot hanging on ninja check-all , which David Candler reproduced on downstream Ubuntu/AArch64 hardware. ProcessPoolExecutor.shutdown(wait=True) calls call_queue.join_thread() before it joins the worker processes with p.join() .On macOS, the feeder thread behind join_thread() can’t finish draining until the workers are joined, so it ends up blocking on a step that hasn’t happened yet.CPython’s own source has carried a comment about this exact macOS deadlock risk since 2018, which is why p.join() exists, it’s just sequenced on the wrong side of join_thread() . Second, lit was submitting every test future to the executor up front.Each submission writes an empty send_bytes(b"") call into the executor’s internal wakeup pipe, and even though the payload itself is nothing, CPython’s framing still costs 4 bytes per write for the length header.The OS pipe buffer defaults to 64 KiB, and 65,536 bytes divided by 4 bytes per write is exactly 16,384 writes before it fills, at which point submit() blocks while holding _shutdown_lock , which the manager thread also needs to drain the pipe ( CPython gh-105829 ).Running check-llvm submits ~64,500 tests, far exceeding that pipe limit. The fix implements windowed submission.lit first submits a window of tests, 8 times the worker count by default ( SUBMISSION_WINDOW_PER_WORKER in run.py ), then waits for completions.Each time a future finishes, lit submits exactly one more test, so the number of outstanding futures never exceeds the window.Since every submission writes one entry into the wakeup pipe and the manager thread drains it as work completes, the undrained writes are bounded by the window rather than by the suite size, so the pipe cannot reach the 16,384 write limit and submit() never blocks while holding _shutdown_lock .The window still exceeds the worker count, so every worker stays busy.After verification across macOS and AArch64 test environments, the relanded migration merged cleanly. Pull Requests migrate lit to ProcessPoolExecutor reland “migrate lit to ProcessPoolExecutor” A Regression on High Core Count Machines In August, Cullen Rhodes reported a scaling regression: check-llvm ran 56% slower on a 64-core AArch64 Graviton2 machine (72.98s vs a 46.70s baseline).Benchmarks on 10-core (macOS) and 16-core (Linux) test setups had shown only a 0.2% to 0.5% difference against baseline, indicating the slowdown was specific to high core count scaling rather than a universal regression. The cause stemmed from concurrent.futures.wait() .Calling wait() re-registers listener callbacks against every outstanding future on each iteration, causing repeated $O(N)$ scanning cost per completion as concurrency grows. Replacing the wait() rescan with a SimpleQueue and add_done_callback() , registered once per future at submit time instead of rescanning the whole pending set, reduced the 56% regression on 64 cores down to ~8%. My mentor, Paul Kirth , built on that, adding worker-side task batching ( execute_batch() ) on top of the same SimpleQueue model.Paul had access to a 64-core Threadripper workstation, so it was easy for him to test scaling well past what I could reach, and he measured check-llvm dropping from 105.2s to 48.38s, 4.6% faster than the pre-migration baseline of 50.7s. Pull Requests collect completed tests via callbacks, not wait() (closed, superseded by Paul Kirth’s fix below) address scaling problems with ProcessPoolExecutor (Paul Kirth) Evaluating Execution Engine Architectures With the ProcessPoolExecutor migration in place, and Part 2 already calling out an asyncio engine as worth trying, I built and benchmarked three alternative execution engine designs to see if process creation overhead or IPC pickling costs could be cut further. Thread-Based Execution ( ThreadPoolExecutor ) Even with the windowed-submission fix above, every test still meant a full process fork plus a pickle round-trip of its Test object and result.A Scalene profile of llvm-mca at -j1 showed the fork/exec syscall alone costing 4.5% of wall-clock time, more than every line of lit’s own Python combined.The combined process-and-pickle dispatch overhead was attributable for 5.23% of wall-clock at -j1 , rising to 22.25% at -j40 as concurrency increased.The most direct way to cut that cost is to stop paying for a process per test at all, and rather use worker threads.In theory, a single process with multiple threads running tests should perform much better. Before testing this, I had to fix three thread-safety issues in lit: os.umask (which is process-global and was mutated around every subprocess launch), the regex compilation cache in _caching_re_compile (an unlocked dictionary), and the per-suite environment dictionary in GoogleTest formats. Once those races were fixed, I benchmarked ThreadPoolExecutor on check-llvm : Apple M5 (10 cores) : 401s with threads versus 216s with processes (1.86x slower) Intel i5-13450HX (Ubuntu 24.04) : 591.21s with threads versus 109.15s with processes (5.42x slower) Profiling with Scalene at -j1 and -j10 revealed why threads performed so much worse than processes.Plain Python bytecode’s share of time barely changed, but native and C extension time rose from 10.28% to 34.12% on macOS, and from 16.35% to 43.79% on Linux, concentrated in subprocess.py , threading.py , and posixpath.py .Because lit spends most of its time spawning and waiting on subprocesses, worker threads continuously contend for Python’s Global Interpreter Lock (GIL).With ten worker threads running concurrently, GIL contention dominated runtime.Until free-threaded Python (PEP 703) becomes standard, threads are not viable for lit. Phase-Pipeline Execution In lit today, a single worker process executes all five phases of a test from start to finish: parsing the test script, applying substitutions, parsing shell syntax, running the commands, and formatting results.I experimented with a phase-pipeline design where each phase runs in its own process, passing work down a queue like a hardware CPU pipeline. Timing execution phases in TestRunner.py showed that the actual execution phase (spawning and waiting on llc or FileCheck ) accounts for 84% to 89% of total runtime.The remaining four phases combined make up only ~10.7% of execution time.By Amdahl’s Law ($\frac{1}{1 - 0.107} \approx 1.12\times$), even if we perfectly overlapped those four phases with zero IPC overhead, the theoretical maximum speedup is about 1.12x. When I built the prototype, wall-clock time came out virtually identical to the default process backend, within 0% to 4% noise.It also required four pickle crossings per test instead of two, and didn’t reduce the process count either.Not worth the added complexity, so I dropped it. Asyncio Single-Process Engine To eliminate worker processes and IPC pickling entirely, I wrote a single-process execution engine using Python’s asyncio . In an asyncio engine, test objects never leave the main process.To prevent asyncio.create_subprocess_exec from spawning a helper thread per child process (which would re-introduce GIL contention), I explicitly set SafeChildWatcher so process reaping happens on the main event loop via signal handlers. The benchmark results depended heavily on core count: At -j1 : 1.8x to 2.3x faster than process pools, because there is no IPC, pickling, or process pool overhead. At -j4+ : 1.5x to 1.9x slower , because a single Python event loop cannot run tasks across multiple CPU cores in parallel. The crossover point falls between -j2 and -j3 on the machine I tested this on.Below that, asyncio wins because there’s no IPC to pay for.Above it, one Python process just can’t keep enough cores fed. Dispatch Chunking I also tested dispatch chunking, where the main process submits multiple tests in a single ProcessPoolExecutor.submit() task call to reduce pickling frequency. Because --order=smart sorts tests by descending historical duration, a contiguous chunk of consecutive tests concentrates several of the slowest tests onto one worker while other workers finish early and sit idle.Chunk sizes above 1 require --order=random instead, to avoid exactly that. On llvm-mca at -j10 , the smart-order baseline was 3.15s.Chunk sizes of 1, 8, and 32 under random order measured 3.29s, 3.14s, and 3.21s.None of that is a difference worth asking reviewers to take on a hard --order=random requirement, so I left it as a draft instead of pushing it through review. Summary of Engine Experiments Engine Design Mechanism Benchmark Performance Result ProcessPoolExecutor (Baseline) Worker process pool with windowed submission Baseline ( check-llvm : ~50.7s) Shipped as default ThreadPoolExecutor Worker thread pool 1.86x to 5.42x slower Rejected (GIL contention) Phase-Pipeline Engine Dedicated process per test phase 0% to 4% difference vs baseline Rejected (adds pickle crossings) Asyncio Engine Single-process event loop 1.8x–2.3x faster at -j1 , slower at -j4+ Not pursued further (doesn’t scale past a few cores) Dispatch Chunking Submitting test batches per worker task No measurable win at -j10 under --order=random Left as a draft Pull Requests migrate lit to ThreadPoolExecutor (closed) add dispatch chunking to the worker pool (draft) Running cat and diff In-Process By late June, cat and diff were the shell builtins spawning external subprocesses, incurring Python interpreter startup overhead for file I/O operations. To run them in-process while maintaining pipeline compatibility ( | and 2>&1 ), I structured the implementation across three PRs: Refactored cat.py and diff.py so main() delegates execution to a run(argv, stdin, stdout, stderr, cwd) function that takes IO streams as arguments. Implemented an InProcessPipe shim (duck-typed to subprocess.Popen ) allowing TestRunner.py ’s pipeline executor to treat in-process builtins transparently alongside spawned tools. Enabled in-process cat and diff execution by default. Benchmarking in-process cat and diff across six builtin-heavy test suites ( UpdateTestChecks , Interpreter , Integer , Feature , ClangScanDeps , and ExtractAPI ) showed median wall-clock runtime dropping from 67.66s down to 63.02s (a 6.9% speedup ) with zero RSS increase and byte-identical test output. Pull Requests add stream-injectable run() core to builtin cat use provided streams in builtin diff run builtin cat / diff in-process instead of spawning The env Short-Circuit Bug LLVM issue #115578 identified a bug where a bare env command with no subcommand returned early from the pipeline executor, silently dropping subsequent commands (causing lines like env | FileCheck to skip validation). The proposal’s plan for this was to separate env processing out of the main command loop.By the time I actually got to it, that wasn’t necessary anymore: the InProcessPipe helper built earlier in the summer for the cat / diff work above gave me a pipeline-stage abstraction that didn’t exist when the proposal was written, so I ran bare env through that instead of building the bespoke fix originally planned.Fixing the bug meant FileCheck finally ran on lines it had always been silently skipping.That surfaced three previously-invisible bugs across six test fixtures: a KEY = VALUE vs KEY=VALUE format mismatch, two check-prefix typos, and one FileCheck-empty-input case needing --allow-empty .All three had been masked by the same class of failure the GitHub issue itself described. Pull Requests stop bare env from short-circuiting the pipeline Overview Cleaned up the legacy Python 2 idioms still left in lit’s codebase. Migrated lit to ProcessPoolExecutor .Found and fixed two CPython concurrency bugs along the way, and helped on the follow-up fix for the high-core-count regression. Tried a thread-based backend.It performed far worse than processes because of GIL contention, and I wrote up why instead of just dropping it quietly. Moved cat and diff to run in-process instead of spawning a subprocess per invocation. Built and benchmarked three more execution engine designs on top of that. Fixed the env pipeline short-circuit bug, which turned up a handful of latent test bugs on the way. Future Work Type Annotations : Adding type annotations across lit to improve maintainability and catch type errors early. Testing PPE under Random Ordering : Evaluating ProcessPoolExecutor with --order=random by default or exploring better load-balancing heuristics for batched execution. Executor and IPC Overhead at Full-Suite Scale : A separate Scalene profile of the full check-llvm suite showed ProcessPoolExecutor ’s own manager thread and the pickled-result IPC pipe write together account for about 71% of CPU time, while lit’s own code stays under 16%.Filed as issue #218095 : the IPC share grows with test count, from about 2% on llvm-mca to about 10% on check-llvm , rather than staying fixed. An earlier, smaller-scale round of Scalene profiling had already turned up three more targeted fixes: LRU Cache regex compilation : Replacing TestRunner.py ’s custom _caching_re_compile with functools.lru_cache saves ~0.10s self-CPU time on llvm-mca .Already upstreamed: use functools.lru_cache instead of lit.util.memoize , followed by removing the now-unused memoize function itself . Directory scanning with os.scandir() : Replacing os.listdir() + os.path.isdir() with os.scandir() avoids redundant stat() calls during test discovery, cutting ~18% of warm discovery overhead on llvm/test and clang/test . Pre-compiling kPdbgRegex : Compiling kPdbgRegex once at module load saves 108MB in allocations and ~0.048s on full llvm-mca test runs. Profiling ruled some things out too.The proposal listed breaking up TestRunner.py , over 2,400 lines in one file, as a possible refactor if profiling justified it.I tried a few substitution-engine rewrites on top of the lru_cache fix above and none of them measured faster than run-to-run variance, so there wasn’t a real case for it, and it’s still one file. What I’ve Learned CPython concurrency internals : chasing the macOS shutdown-ordering deadlock and the wakeup-pipe deadlock forced me to actually learn how CPython handles multi-process shutdown and IPC, not just treat concurrent.futures as a black box. Syscall overhead and the GIL : Scalene made it obvious why threads lost.Syscall-heavy work spends most of its time exactly where threads can’t help. Duck-typed abstractions : InProcessPipe mimicking subprocess.Popen meant the rest of the pipeline code didn’t need to know or care that cat and diff weren’t spawning a process anymore. Beyond GSoC, I’d like to keep contributing to LLVM’s testing infrastructure, particularly digging into the executor and IPC overhead from issue #218095 , and the type-annotation pass listed under Future Work above. I’ve also been reading up on MLIR on the side this summer, working through it with an LLM whenever I got stuck, and I’d like to get more directly involved there next, especially on the GPU-facing dialects.It’s a jump from testing infrastructure to compiler internals, but it’s the direction I actually want my work to go in. Acknowledgements Thank you to my mentors, Aiden Grossman , Paul Kirth , and Petr Hosek , for their guidance throughout the summer. Thanks also to Douglas Yung and David Candler for reporting and verifying the macOS/AArch64 shutdown fixes, Cullen Rhodes for identifying and verifying the high core count scaling regression, and Alexander Richardson for reviewing several pull requests throughout the summer. Thank you to the LLVM Foundation for organizing everything, and to Google Summer of Code for the opportunity.📝The LLVM Project Blog
EngFlow is a Cursor Origin launch partnerEngFlow is a Cursor Origin launch partner Bazel makes your builds correct and reproducible. Making them fast, and keeping them fast as your codebase and your team grow, is a different problem. It's the very problem that EngFlow was created to solve. Our team spent years on the adoption side of Bazel, helping companies put it into production, contributing and creating Bazel's core features, and the pattern is almost always the same: teams love what Bazel does for correctness, then run straight into what it costs to operate for speed. Today, that's much easier to reach. EngFlow, the platform that runs the largest managed Bazel fleet in the world, with over 3 billion build and test actions every week for teams like Arm, Databricks, and Canva, is now one install away for every team on Origin. Read more about this in Origins's announcement on X (Twitter) and Linkedin .📝EngFlow Blog
Who pays for ClangClang's funders have changed since 2010: Apple and Google, which wrote most of it until the mid-2010s, now write a small share, and GPU and accelerator vendors, Red Hat, and new sponsors of C++ conformance work write more. This post counts commits, pull-request approvals, and the kind of code each employer touches. The method is unscientific: commit counts are a crude proxy for work, and the attribution is my best guess, so read the numbers as rough trends, not an authoritative record. It grew out of ClangIR and the cost of MLIR , which needed it to explain who wanted an MLIR-based Clang. Like that post, this one relies on an LLM agent (Claude Code), which collected the git data, looked up affiliations, and computed the tables; I checked and edited the results. Commits by employer Commits under clang/ by who paid for the work, usually the author's employer at the time. "Top ten" is the share of commits by the ten most active authors: Year Commits Authors Top ten Google Apple Intel Red Hat NVIDIA AMD Arm Microsoft other companies universities and labs individuals or unknown 2010 8,191 72 77% 5% 79% 0% 0% 0% 0% 0% 0% 3% 6% 7% 2012 7,545 135 55% 23% 54% 0% 0% 0% 0% 0% 0% 7% 3% 12% 2014 6,347 200 45% 43% 18% 3% 0% 0% 0% 2% 0% 18% 1% 15% 2016 5,253 305 30% 38% 21% 5% 0% 0% 2% 2% 0% 15% 1% 17% 2018 4,973 351 32% 30% 19% 8% 0% 0% 2% 2% 0% 17% 4% 18% 2020 5,523 551 25% 29% 10% 4% 1% 0% 5% 7% 0% 19% 2% 22% 2022 6,193 677 27% 21% 7% 8% 6% 0% 3% 4% 2% 16% 6% 27% 2024 7,330 882 31% 12% 6% 5% 12% 4% 6% 4% 3% 19% 1% 28% 2026 (to October) 7,160 1,024 25% 7% 9% 4% 8% 11% 10% 3% 3% 12% 3% 30% I took the employer from the commit email when it is a company address. For the authors who used personal addresses, which before the 2019 move to GitHub was most of them, I looked up dated employment for about 150 of the most active ones from LinkedIn, Developers' Meeting speaker lists, paper affiliations, and GitHub profiles. Work done in spare time or unrelated to the employer counts as individual, such as Craig Topper's X86 work before Intel paid for it and my own Clang work while I was at Google. "Individuals or unknown" is independent contributors plus authors I could not place; people with ten or fewer commits each make up about half of it in 2024 and three-fifths in 2026, and the rest is a few prolific individuals such as Owen Pan (clang-format) and me. Before 2019, the recorded author is the committer, who often landed other people's patches, so the early author counts are low. None of this is rigorous: a commit can be a one-line fix or a month of work, public profiles are incomplete or out of date, employment dates are approximate, and a difference of a few points is within the error. Corrections are welcome. What each funder works on Google and Apple wrote 84% of Clang commits in 2010 and 61% in 2014, but 16% in 2026. Apple built Clang for Objective-C and its platforms' toolchain; Google's priorities were large C++ code bases, compile time, and tooling. Red Hat's share is mostly Timm Bäder's bytecode constant expression interpreter. NVIDIA's is mostly CIR; outside CIR it wrote 3% of commits. Its Clang people joined from elsewhere: Erich Keane from Intel in August 2023, Andy Kaylor from Intel in January 2025, and Reid Kleckner from Google in December 2025. AMD's is mostly AMDGPU, HIP, and offloading work, and since 2026 also CIR (14% of its commits). Intel's share includes Aaron Ballman, Clang's lead maintainer; Intel's ICX compiler is built on Clang. Intel's commits are increasingly specific to a language mode or target (SYCL, OpenMP, CIR, OpenCL, X86): 22% in 2024, 58% in 2026. Language conformance work comes from new sponsors. Bloomberg has sponsored Corentin Jabot's Clang work since 2022, and in 2024 also paid Vlad Serebrennikov, who " used to contract for Bloomberg ", and Matheus Izvekov, who wrote papers for Bloomberg ; the three wrote 4.4% of 2024's commits, about the same as NVIDIA's 3.7%. The C++ Alliance now employs Krystian Stasiowski and Matheus Izvekov. The same count over llvm/ shows that the shift is not specific to Clang. The employment lookups covered Clang contributors, so more llvm/ authors are unknown: llvm/ Commits Authors Google Apple Intel Red Hat NVIDIA AMD Arm Microsoft other companies universities and labs individuals or unknown 2010 13,245 97 5% 67% 0% 0% 0% 0% 0% 0% 0% 2% 26% 2014 12,423 273 26% 19% 1% 0% 0% 7% 3% 0% 20% 2% 22% 2018 14,737 494 18% 7% 11% 1% 0% 4% 5% 0% 22% 0% 31% 2022 18,538 946 15% 6% 2% 7% 0% 10% 6% 1% 21% 2% 31% 2026 (to October) 18,539 1,561 10% 7% 2% 3% 7% 20% 7% 1% 15% 2% 28% Google and Apple went from 72% of llvm/ commits in 2010 to 17% in 2026, close to their 84% to 16% in clang/ . What is particular to Clang is Red Hat (8% of clang/ commits in 2026, 3% of llvm/ ) and the CIR part of NVIDIA's rise: NVIDIA grew in both, to 11% of clang/ and 7% of llvm/ . AMD is the largest funder of llvm/ , at 20%. Review Commit counts undercount review, and cleanup commits inflate some authors: Kazu Hirata's mechanical cleanups were 158 of Google's clang/ commits in 2024 and 326 in 2025 (24 in 2026). For 2026 I also counted approvals on the 7,005 merged pull requests that touch clang/ , each approver once per pull request (8,333 approvals). Reviewers are attributed the same way, with the GitHub profile's current company as a fallback. Authorship matches the 2026 commit shares above; "CIR" means the 1,285 pull requests whose title is tagged CIR: 2026 pull requests Google Apple Intel Red Hat NVIDIA AMD Arm Microsoft other companies universities and labs individuals or unknown approved 8% 13% 7% 2% 17% 10% 3% 6% 21% 3% 10% authored, excluding CIR 8% 11% 5% 10% 3% 11% 2% 4% 12% 3% 30% approved, excluding CIR 10% 17% 8% 2% 7% 11% 3% 8% 22% 3% 10% Individuals write 30% of the pull requests but give 10% of the approvals; review is concentrated in fewer, mostly company-paid people. The ten most active approvers give 36% of the approvals, and 24 people give half, about as concentrated as commits were in 2016–2018. Outside CIR, the most active approvers are Corentin Jabot (Bloomberg), Aaron Ballman (Intel), Balázs Benics (Apple, the Static Analyzer), Gábor Horváth (Apple, mostly lifetime safety), Joseph Huber (AMD), Eli Friedman (Qualcomm), and Erich Keane (NVIDIA). Inside CIR, NVIDIA gives 57% of the approvals, mostly Andy Kaylor and Erich Keane. Shared and vendor-specific work Not all of this work is shared. I split the 2026 commits by the code they touch: "specific" is one language mode (HLSL, OpenCL, CUDA/HIP, SYCL, OpenMP, OpenACC), CIR, one target, or one OS's driver; "tools" is the Static Analyzer, clang-format, Tooling, libclang, and clang-repl; "core" is the rest, such as Sema, AST, the parser, modules, and target-independent CodeGen. The majority of a commit's files decides, and its title tag breaks ties. The rule matters: other reasonable rules move the vendor cells by up to 8 points (Intel's ranges from 50% to 65%). 2026 Google Apple Intel Red Hat NVIDIA AMD Arm Microsoft other companies universities and labs individuals or unknown specific, of the employer's commits 17% 5% 58% 3% 86% 68% 79% 86% 36% 44% 33% share of core commits 11% 13% 3% 17% 3% 6% 1% 1% 11% 3% 32% share of approvals on core pull requests 12% 20% 9% 3% 8% 7% 1% 1% 25% 3% 10% Measured by the code touched, NVIDIA's, AMD's, Arm's, and Microsoft's commits are 68–86% specific to a language mode or target. The shared core is written mostly by individuals, Google, Apple, and Red Hat, and six people give a third of its approvals. In 2024, 23% of all clang/ commits were specific; in 2026, 41%. CIR, which went from 14 specific commits to 1,336, accounts for more than the whole increase; other specific work fell slightly. Vendor-specific does not mean niche: AArch64 and x86 work reaches most users, and AMDGPU and HLSL matter to their own users. The difference is who reads the code and keeps it working. Specific code is mostly maintained by the people who need it, and its cost to everyone else is building it and occasionally fixing a test; CIR is the case where that cost became large.📝MaskRay

Sunday, October 11, 2026

Why are my builds slow? Using CMake’s Instrumentation FeatureCMake 4.2 dropped the experimental gate on a major new feature: the CMake Instrumentation API. This enables detailed tracking of the entire CMake workflow, including configure time, build execution, testing, and installation, and provides developers and teams with actionable insights into build performance. Support for automated, user-written scripts to handle instrumentation data make it easy to track performance data over time across different machines and environments.📝Kitware Inc
C++26 Contracts: Introduction to Preconditions and PostconditionsIn the article about Standard Library hardening , we looked at checks for invalid Standard Library container access and other broken preconditions. I mentioned that those requirements are expressed in terms of “contracts”. But what about our code? In this post, we’ll explore contracts from C++26. We’ll start with a simple cassert , move the requirement into the function itself, and see how to check results. We’ll also look at declarations and definitions, compiler options, and a few details that can surprise you. Starting with an Assertion Let’s start with a small example: A header file first: // foo.h #include int get_value ( const std :: vector int >& values , std :: size_t index ); And the implementation: // foo.cpp #include "foo.h" // our function declaration... #include int get_value ( const std :: vector int >& values , std :: size_t index ) { assert ( index values . size ()); return values [ index ]; } The function expects a valid index. If the condition is false, assert prints a diagnostic message and terminates the program. However, this only happens when assertions are enabled. Defining NDEBUG disables the check and basically removes the check from the code. What are the issues with this simple approach? The requirement lives inside the implementation. Somebody reading only the declaration sees a vector and an index, without the relationship between them. Adding a Precondition With C++26 contracts, we can move that requirement from an assertion inside the function body to the function itself: int get_value ( const std :: vector int >& values , std :: size_t index ) pre ( index values . size ()) { return values [ index ]; } Notice the new pre(...) syntax after the parameter list. It introduces a precondition assertion : a condition expected to hold when entering the function. What’s more, we can add this precondition to the declaration: int get_value ( const std :: vector int >& values , std :: size_t index ) pre ( index values . size ()); The feature was introduced through P2900, Contracts for C++ . Let’s understand how it works and how it differs from assert . Why Use Contracts Instead of assert ? At first, pre(index might look like another way to write assert(index . Both express a condition that should be true. So what’s the benefit of using contracts? There are a few important differences: Requirements in declarations: We can put pre and post directly in the function declaration. This makes the requirements visible to anyone reading the API, without looking at the implementation. More flexible checking: Traditional assert is enabled or disabled through NDEBUG . Contracts define several evaluation semantics, including ignoring a check, reporting a violation and continuing, or terminating the program. The compiler implementation determines which semantics are available and how they can be selected. Preconditions and postconditions: Contracts distinguish between what the caller must provide and what the function promises to return. With assert , we would have to place checks manually inside the function body, including checks before different return statements. This also makes contracts useful for documenting APIs, even when runtime checking is disabled. A Complete Precondition Example Here’s a complete example (no header file for simplicity for now): #include #include int get_value ( const std :: vector int >& values , std :: size_t index ) pre ( index values . size ()) { return values [ index ]; } int main () { std :: vector int > values { 10 , 20 , 30 }; std :: println ( "{}" , get_value ( values , 1 )); // get_value(values, 10); // violates the precondition } The valid call prints: 20 And here’s the version for you to play with the problematic call uncommented: See at Compiler Explorer On GCC, I’m getting: Program returned: 134 Program stdout 20 Program stderr contract violation in function int get_value(const std::vector &, std::size_t) at /app/example.cpp:6: index Contracts on Declarations and Definitions For clarity, contracts can appear on a declaration, in a definition, or both: int get_value ( const std :: vector int >& values , std :: size_t index ) pre ( index values . size ()); The definition can then omit the contract specifier: int get_value ( const std :: vector int >& values , std :: size_t index ) { return values [ index ]; } The contract assertions are established by the function’s first declaration. A later declaration or definition may omit the contract specifiers, as above, or repeat the same sequence. A later declaration cannot introduce a different set of preconditions or postconditions. In practice, if a function is declared in a header, that first declaration is the natural place for its preconditions and postconditions. If you’re reading the API, it’s probably best to see all preconditions without looking inside the implementation. What’s more, contracts are associated with the function, but they are not part of the function type. Overload resolution, for example, does not distinguish functions based on their contracts. Contract Evaluation Semantics C++26 defines four evaluation semantics: Semantic What happens at runtime? Ignore The predicate is not evaluated. Observe A violation invokes the handler; execution continues if it returns normally. Enforce A violation invokes the handler; execution terminates if it returns normally. Quick-enforce A violation terminates execution without invoking the handler. The implementation determines which semantic applies. C++26 has no portable syntax for forcing an individual ordinary contract assertion to use one specific policy. These rules appear in the contract evaluation specification . With the ignore semantic, no runtime check is performed. With observe, a violation is reported, but execution continues if the handler returns normally. In our example, both policies can allow the program to reach values[index] with an invalid index, resulting in undefined behavior. Thus, adding pre(...) expresses the requirement, while the selected policy determines how checking responds. Compiling with GCC As of October 2026, only GCC 16 supports Contracts. We can use it with special compile flags: g++ -std = c++26 -fcontracts \ -fcontract-evaluation-semantic = enforce example.cpp With that policy, the invalid call invokes the violation handler. If the handler returns normally, execution terminates before the function body performs the invalid access. GCC’s default handler emits diagnostic information. See the GCC options documentation - contracts . GCC also allows us to replace the default contract-violation handler. For example, we can log a custom message when a violation is detected. The handler receives a std::contracts::contract_violation object, which provides information about the failed contract, including the predicate, location, and evaluation semantic. #include #include void handle_contract_violation ( const std :: contracts :: contract_violation & violation ) { std :: println ( stderr , "Contract violated: {}" , violation . comment ()); } See an example @Compiler Explorer Notice that replacing the handler doesn’t change what happens after it returns. With observe , execution continues. With enforce , the program terminates. Not every implementation is required to support replacing the handler, so this feature is implementation-defined. Adding a Postcondition So far, we’ve described what the caller must provide. We can also describe what a function promises when it returns normally. See this example: int clamp_value ( int value , const int low , const int high ) pre ( low high ) post ( result : result >= low && result high ) { if ( value low ) return low ; if ( value > high ) return high ; return value ; } There are two useful pieces here: pre(low checks that the bounds form a valid interval. post(result: ...) checks that the returned value lies inside that interval. The name result is ours to choose. It refers to the function’s result within that postcondition. What’s convenient is that the same postcondition covers every normal return path. We don’t have to repeat an assertion before each return . Here’s an example where I deliberately violated the postcondition: #include int clamp_value ( int value , const int low , const int high ) pre ( low high ) post ( result : result >= low && result high ) { if ( value low ) return low - 10 ; // if ( value > high ) return high ; return value ; } int main () { std :: print ( "{}" , clamp_value ( 10 , 20 , 30 )); } See @Compiler Explorer And on GCC I’m getting: contract violation in function int clamp_value(int, int, int) at /app/example.cpp:5: result >= low && result [assertion_kind: post, semantic: enforce, mode: predicate_false, terminating: yes] terminate called without an active exception Program terminated with signal SIGABRT (6) The precondition is fine, but the postcondition is broken. Const Parameters in Postconditions There’s also a detail in the parameter list: low and high are const . You will get the following error if you try making them non-const: source>:5:28: error: a value parameter used in a postcondition must be const 5 | post(result: result >= low && result When a postcondition uses a parameter passed by value, that parameter must have const type on every declaration. Our comparisons use both bounds this way. The parameter value is only used in the body, so this requirement doesn’t apply to it. Postconditions apply to normal exit; they do not check the result when the function body exits through an exception. See the function contract rules . Assertions Inside a Function For conditions inside an implementation, we have contract_assert : #include std :: size_t count_spaces ( std :: string_view text ) { std :: size_t count = 0 ; for ( char ch : text ) { if ( ch == ' ' ) ++ count ; } contract_assert ( count text . size ()); return count ; } The condition is simple, but it expresses a useful property: the number of spaces cannot exceed the number of characters. In a larger algorithm, such a check could describe a relationship between counters, an intermediate result, or the state after processing a block of data. Together, pre , post , and contract_assert cover function entry, normal exit, and points inside a function. Avoiding Mutation in Predicates One thing to watch during migration is mutation inside predicates: void process ( int count ) pre ( ++ count > 0 ) // error { } See @Compiler Explorer An error from GCC: error: increment of read-only location '(const int)count' 2 | pre(++count > 0) // error Contract predicates use special constification rules. In this example, count is treated as const inside the predicate, so ++count is rejected by the compiler. However, this does not make predicates completely free of side effects. For example, a predicate can still call a function that modifies global state. Since contract evaluation may be ignored, and side effects are not guaranteed even under checking semantics, predicates should not modify program state. Contracts Are Not Input Validation There’s another practical distinction: input validation. Suppose an index comes from a configuration file. If an invalid value requires an error message, that handling belongs in ordinary control flow: if ( index >= values . size ()) { std :: println ( "Invalid index: {}" , index ); return ; } std :: println ( "{}" , get_value ( values , index )); The validation handles an expected failure at the application boundary. The precondition documents the internal helper’s requirements. P2900 explicitly discusses this idea in: Principle 12: Contract Assertions Are Not Flow Control: While a contract assertion provides an algorithm to validate correctness, nothing about a contract assertion guarantees any particular runtime behavior associated with that syntactic construct. And more: Importantly, this aspect of Contracts is why contract assertions must not be used for error handling and input validation: If a function has in-contract requirements to report certain events as errors, that handling must be done with standard C++ control statements that are not optional, never with contract assertions. Considering Predicate Costs Finally, one note about performance implications. Our index comparison is cheap. A condition that verifies whether an entire collection is sorted needs to examine its elements. An expensive check may be useful during testing, but you should measure its effect on representative workloads. Summary C++26 contracts give us three ways to express assumptions and guarantees directly in the language: pre(...) describes conditions expected to hold when a function is entered, post(...) describes conditions expected to hold when a function returns normally, contract_assert(...) checks properties at specific points inside a function. Preconditions and postconditions are associated with the function through its declarations. In particular, the first declaration establishes the contract, which makes declarations in header files a natural place to document an API’s requirements. A contract assertion does not necessarily mean that a runtime check will always be performed. The selected evaluation semantic can ignore, observe, enforce, or quick-enforce the assertion. Code therefore shouldn’t depend on contract predicates for required application logic or side effects. Contracts also aren’t a replacement for normal input validation. Use regular control flow for errors that your program expects and needs to handle. References P2900, Contracts for C++ Contract assertions (since C++26) - cppreference.com - C++ Reference How C++26 Contracts Improve Correctness Without Sacrificing Performance | Citadel Securities How C++26 contracts work | Train IT Test Driving C++26 Contracts with GCC 16.1 | Fabian Koehler📝C++ Stories
ClangIR and the cost of MLIRClangIR (CIR) is an MLIR dialect between the AST and LLVM IR, and in October 2026 the clang area team accepted building it by default . CIR is built on MLIR because each step along the way was locally reasonable, and together the steps locked MLIR in. The costs (build time, binary size, MLIR API churn) land largely on Clang contributors who neither chose MLIR nor use CIR. This post traces how CIR came to depend on MLIR, collects what the dependency costs, and uses an MLIR-free CIR as a thought experiment to separate the costs inherent to MLIR from those that can be reduced. This post relies heavily on an LLM agent (Claude Code). It read the Discourse threads, GitHub PRs, slides, and the source tree, computed the build-closure and contributor numbers, and drafted the text, which I then edited. My part was steering, and some of what I added to its context: the build-by-default and premerge-testing RFCs, and a discussion on Discord #llvm; the opportunity-cost framing: contributors without fast machines pay for MLIR (and Flang) on every llvm/ change, though most of them never use either. Treat the numbers as checked but not peer-reviewed. What ClangIR replaces Classic Clang lowers the AST to LLVM IR in clang/lib/CodeGen . With -fclangir , clang/lib/CIR/CodeGen (CIRGen) emits CIR instead, a set of CIR passes runs, CIR is converted to MLIR's llvm dialect, and MLIR translates that to LLVM IR. CIRGen mirrors the AST-to-IR half of clang/lib/CodeGen file by file: CodeGenFunction becomes CIRGenFunction , CGExprScalar.cpp becomes CIRGenExprScalar.cpp , and so on. It is 66K lines against CodeGen's 214K, with 786 errorNYI calls marking what is missing. Work that classic CodeGen does while emitting IR becomes separate CIR passes: C++ ABI lowering ( CXXABILowering ), calling conventions ( CallConvLowering ), exception handling ( EHABILowering ), and structured control flow to a CFG ( FlattenCFG ). The optimizer and backend are not replaced. CIR's frontend action calls emitBackendOutput from clang/lib/CodeGen/BackendUtil.cpp , so clangCIRFrontendAction links clangCodeGen . Some code is shared rather than duplicated: clang/lib/CodeGenUtils ( #221001 , September 2026) holds AST-only helpers used by both, and llvm/lib/ABI (LLVMABI) does ABI classification for CIR's calling-convention lowering and, under -fexperimental-abi-lowering , for classic CodeGen. Nothing in clang/lib/Analysis is replaced yet. The CFG, the analyses behind Sema's analysis-based warnings, the dataflow framework, lifetime safety, and the Static Analyzer all still work on the AST and the CFG. Upstream CIR's only analysis is an alias analysis used by ABI lowering. Replacing CFG-based analysis is an aspiration rather than a plan: the 2022 RFC argued that the CFG is "discarded after analysis" and "has lots of known problems", and the area team expects CIR to replace "the current CodeGen and CFG analysis", but no ClangIR document schedules it. The in-tree roadmap, clang/docs/CIR/CodeDuplication.md , is about CodeGen. It plans to raise CIR to a higher level, move ABI details into later lowering passes, and "replace relevant parts of the existing implementation, piece by piece" with implementations shared by both code generators. Calling conventions are the first case (LLVMABI, plus an MLIR adapter that FIR can also use). The roadmap lists the next candidates: constant expression evaluation, complex multiplication and division, builtins, exception handling and C++ cleanups, inline assembly, and C++ ABI handling (vtables, virtual calls, constructor and destructor arguments, dynamic casts, base class addresses, type descriptors, array new and delete). Linkage, alignment, debug info, TBAA, sanitizers, and lifetime markers are spread through CodeGen and listed as harder to share. It also aims to run all classic CodeGen tests through the CIR path. Where things stand The area team's decision says "ClangIR is likely to reach the point where it becomes the replacement for the current CodeGen and CFG analysis." The same post promises that -DCLANG_ENABLE_CIR=Off "will still be a supported configuration for the forseeable future", and -fclangir stays off by default. On main, clang/CMakeLists.txt tests CLANG_ENABLE_CIR but declares no option() for it, so the flip is still pending. A separate RFC, Add CIR testing to clang and MLIR CI ( #227834 ), would build and test CIR in premerge for clang/ and mlir/ changes. nikic is weakly -1 : if contributors have to fix CIR failures, "they also need to build and test CIR locally", so premerge testing and build-by-default "should go hand in hand". rengolin calls asking non-CIR developers to stub out CIR breakage "an unreasonable request" and suggests a different CI setup. As for the compiler itself, the ClangIR progress report for September 2026 measures -fclangir against classic CodeGen: 1.32x the wall time on a typical file, 1.57x across the tree, 1.17x the memory, and 3% more code. Why MLIR Meta, 2022: framework reuse for a lifetime checker ClangIR started at Meta in October 2021. Bruno Cardoso Lopes and Nathan Lanza proposed it in RFC: An MLIR based Clang IR (CIR) in June 2022, and it moved to an LLVM incubator in August 2022 (timeline from the 2025 keynote ). The primary goal was "improved diagnostics for modern C++"; the design had "grown out of implementing a lifetime analysis/checker pass"; optimization and lowering were "a secondary goal for now". Clang lacked a suitable IR: the AST is awkward for dataflow, LLVM IR is too low level, and the Clang CFG is discarded after analysis. The RFC's entire "Why MLIR?" section reads: MLIR provides a solid and tested framework to build custom IR and run passes. Among several other capabilities, it's also in already in tree and used by Flang to build FIR. Nobody in the thread argued for a bespoke IR; the closest was John McCall's aside that if MLIR needed in-tree dialects to be useful, "it would be better to just not use MLIR" and write the IR "from scratch on top of llvm::ilist". efriedma wrote "using MLIR to build it seems like the right approach" and objected to structured control flow instead. Gábor Horváth argued that the CFG suffices for lifetime analysis, and John McCall wanted a two-stage lowering like Swift's. Reid Kleckner asked for an agreed compile-time ceiling, "like +15-25%, before making this part of the default pipeline". -fclangir is still opt-in, and 1.32x/1.57x is already past that ceiling. Lopes's later talks do not revisit the choice. The 2023 Developers' Meeting talk ( video ) describes CIR as "Implemented as an MLIR dialect for C/C++". Its pipeline diagram goes from CIRGen through CIR passes, including the lifetime checker, to LLVM lowering, with a dashed side path through SCF and Affine. The lifetime checker was real: the talk reports integration with clangd, clang-tidy, and an internal IDE, and "Successful detection of lifetime bug that caused a major internal outage". The same talk lists "Compile & execution time, binary size, memory usage" among the challenges. The project site gives today's rationale: MLIR enables "the development of passes and IR, rapid design iteration, and reuse of community-provided analyses and transformations". Apart from a few OpenMP passes, clang's CIR pipeline runs only CIR's own passes. The lifetime checker never reached upstream CIR. Clang's lifetime-safety analysis ( #142313 , July 2025) is built on the CFG, and Horváth is among its authors. NVIDIA, 2023: OpenACC lives in MLIR In October 2023 Erich Keane (NVIDIA) posted RFC: Implementation of OpenACC 3.3 for offload in Clang . Flang lowers OpenACC through MLIR's acc dialect: "It is our intention to use that as well, so NVIDIA is beginning to participate in the CIR/MLIR effort". Clang shares more than the dialect with Flang. CIR and FIR implement the same acc type interfaces ( CIROpenACCTypeInterfaces.cpp , FIROpenACCTypeInterfaces.cpp ), and acc-to-GPU passes are landing in mlir/lib/Dialect/OpenACC/Transforms . Classic CodeGen has no OpenACC lowering ( CodeGenFunction.h : "TODO OpenACC: Implement this"), so CIR plus acc is Clang's only OpenACC implementation. Strictly, the requirement was MLIR's acc dialect, not CIR. Keane's fallback was "LLVM-IR based metadata to enable the translation to the OpenACC MLIR dialect", and the second fallback , "lowering as OMP does", gives up sharing with Flang. OpenMP has the same sharing benefit without the necessity: CIR emits omp ops, but classic CodeGen already lowers OpenMP. By 2026 the motive had widened. Keane wrote that the goal is to "enable further advancement for our compiler stack … the way to allow those to integrate with C/C++/Clang is to have a dialect for it". 2024–2026: lock-in Lopes proposed upstreaming in January 2024, Lanza landed the scaffolding in April 2024, and NVIDIA's first upstream CIR commit (David Olsen) followed in November 2024. Today CIR is 120K lines (lib + include) on ODS, MLIR interfaces, and MLIR rewrite drivers, with ~230 ops and 1,269 test files, 868 of which check -emit-cir output in MLIR's textual format. Readers might ask whether NVIDIA led the upstreaming. I counted commits touching clang/lib/CIR , both upstream and in the archived llvm/clangir incubator (excluding incubator commits that also exist upstream). Affiliation comes from email domains, GitHub profiles, and LinkedIn; a GitHub profile shows the current employer, not the employer at commit time. Author Incubator only (2,498, 2021-08..2026-02) Upstream (2,023) Meta 55% 1.5% NVIDIA 2.7% 47% Amr Hesham (GSoC 2025, then kept going) 5% 14% unknown 28% 27% Meta wrote the incubator: its share was 100% in 2021, 97% in 2022, about half in 2023–2024, and 11% in 2025. NVIDIA wrote 45–49% of upstream commits each year, so by volume NVIDIA led the upstreaming; by authorship of the RFC and the scaffolding, Meta did. Of the 260 upstream commits titled "upstream …", the explicit ports from the incubator, Amr Hesham wrote 37%, NVIDIA 31%, and unknown authors 31%. Most NVIDIA commits are new code written upstream. The maintainers are Andy Kaylor (NVIDIA), Bruno Cardoso Lopes (Meta), and Henrich Lauko (NVIDIA since August 2026, previously Trail of Bits and NextSilicon). The stated long-term value is mixing dialects. From Lopes's SC24 talk (November 2024) on, the slides describe CIR as a way to "Move Clang onto the MLIR substrate" and "Use MLIR from C, C++ and extensions", and to "Mix CIR with downstream and custom dialects". The 2025 keynote with Andy Kaylor lists "Lower to other MLIR dialects" among CIR's uses, next to the lifetime checker, and has a slide titled "CIR Won't Always Go To LLVM IR", where CIR is consumed by MLIR to "Offload to GPU or other targets". clang/docs/CIR/CodeDuplication.md : "even now we generate the ACC (OpenACCC) [sic] dialect in combination with CIR, and when support for OpenMP and CUDA are added, similar mixing will occur". The area team's acceptance ratifies this direction. Every year of upstreaming raised the cost of switching, and the main funder's motive now depends on MLIR itself, not just on the original team's choice of framework. Promises and delivery CIR's case has been made in RFCs and talks since 2022. Here is what was promised and where it stands upstream in October 2026. Promise Source Upstream today Diagnostics: a lifetime checker, coroutine checks, std::optional dereference checks, remarks on expensive copies, cross-TU summaries 2022 RFC None. The incubator's lifetime checker, points-to and live-object analyses, and clang-tidy module were archived in February 2026; Kaylor's transition post lists the analysis passes as not yet upstreamed. Lifetime safety landed on the CFG. Objective-C "in the future" 2022 RFC No Objective-C CIRGen; those paths hit errorNYI . A bridge to Polygeist 2022 and 2024 RFCs None; ThroughMLIR stayed in the incubator. Compile-time benefits from MLIR's "function pass level parallelism" 2024 RFC CIR's passes are scheduled on the whole module, so the pass manager has nothing to run in parallel. It "wouldn't be feasible to ever flip the switch without proper compile time due diligence and improvements" Lopes, 2024 Build-by-default was accepted at a claimed 1.2x; nikic measures +50% on c-t-t. On breakage, "the burden should be on CIR folks to catch up, no revert needed"; expectations on other maintainers: "none" Lopes, 2024 A patch that breaks CIR "would require 'fixing' ClangIR tests" ( 2026 ), and premerge testing is proposed for every clang/ and mlir/ change. "low hanging fruits" and "New CIR passes and optimizations" AsiaLLVM 2025 LibOpt is a 75-line skeleton; IdiomRecognizer recognizes a few library calls such as strlen . "optimization passes that have shown SIGNIFICANT improvements in certain workloads" Keane, 2026 None upstream; Keane describes "admittedly only a few months of work" by an intern. "Eventually we'd like to run all existing codegen tests with CIR enabled" 2025 keynote Pending. Modules, debug info, sanitizers, and non-x86 targets, "work we intend to progress on" Keane, 2026 In September, -g produced no debug info under -fclangir , and the progress report had "Still no ARM64 numbers". The functional milestones have been met, some on schedule. The MultiSource test suite passes, and the stage-2 self-build Keane predicted "within another month" in September works: all 4,744 translation units compile, and Lopes reports check-clang passing on the self-built clang. Shared AST queries moved to clang/lib/CodeGenUtils ( #221001 ), ABI lowering has a shared library, and unimplemented features produce errorNYI diagnostics, as the 2024 RFC planned. What has not arrived is the reason the project was started, better diagnostics, and the optimizations that later took its place, along with the promises about the cost to everyone else. The costs Build aengelke's -1 in the build-by-default thread starts from "I usually don't build MLIR". His measurements of ninja clang in a dylib build track the trimming work: Date Landed before CPU time Wall time c-t-t build time 2026-09-05 +130% +155% 2026-09-09 #221616 , #221813 +40% +83% +58% 2026-09-14 #222576 +30% +71% +47% My #221616 links only the MLIR dialects CIR uses; on 192 cores PragmaTwice measured it alone taking ninja clang wall time from +109% to +75%. #221813 shards the LLVM dialect's generated code, and #222576 gives MLIRIR a precompiled header. On 2026-09-14, libclang-cpp.so in the dylib build was still +91% (+83% stripped); on c-t-t, the clang binary was +51% (+68 MiB) and the build's max-rss +40%. The area team's "roughly 1.2x for a build only building clang" is wall time of a parallel build, which hides much of the CPU cost. The c-t-t comparison for the "enable clangir" commit lists per-file compile times. Summed, they add 1,644 s (+48%): 751 s for CIR's own 100 object files ( CIRDialect.cpp.o alone takes 44 s) and 893 s for MLIR (853 s in 380 object files, the rest in its precompiled headers and TableGen tools). These sums measure work, not wall time. clang still links a closure of 90 MLIR libraries, and some are unused: MLIRArithToLLVM depends on MLIRArithTransforms for one helper, populateCeilFloorDivExpandOpsPatterns ( #118839 ), which drags in Bufferization, SparseTensor, Tensor, Vector, and Shard (~177 s); see Reducing the cost . MLIROpenACCDialect depends on MLIRGPUDialect (~38 s, #174671 ). PDL (~21 s) can be disabled with the existing MLIR_ENABLE_PDL_IN_PATTERNMATCH option. The often-cited NVVM and ROCDL dialects ( NVVMDialect.cpp.o 108 s and ROCDLDialect.cpp.o 134 s in aengelke's 2026-09-05 list) are no longer in the closure after #221616; only 3.3 s of their TableGen output remains, via the mlir-headers umbrella target. MLIR's headers are heavy. For files under tools/mlir/ , the frontend is 75% of compile time and header parsing 55% (aengelke's numbers in #213412 ). MLIRIR now has a precompiled header, but CIR's own libraries do not, and AddLLVM.cmake lets a library reuse the precompiled header of at most one direct dependency. CIRGen reuses clangAST's, so each of its files reparses the MLIR and CIR dialect headers; a private precompiled header like clangCodeGen's ( #206018 ) cuts CIRGen's summed compile time by 72%. Contributors without a build farm Most of the numbers above come from many-core machines, and wall time on a many-core machine hides the cost. On aengelke's 96-thread machine, ninja clang used 30% more CPU time but 71% more wall time, because a few slow files dominate the critical path (on c-t-t, CIRDialect.cpp.o takes 44 s and MLIR's OpenACC.cpp.o 31 s). With fewer cores, wall time approaches CPU time divided by the core count: the extra 2,100 CPU-seconds of that build are 4–5 minutes on 8 threads, for every clean build. Incremental builds matter more to someone iterating on a patch. From the c-t-t per-file times and the header dependencies of my MLIR-enabled build: Linking clang is 38% slower on c-t-t (105 s to 145 s); with lld it stays under a second. Touching llvm/ADT/DenseMap.h , StringRef.h , or raw_ostream.h recompiles 2,504 files (3,274 s summed) without CIR; MLIR adds 380 files (853 s) and CIR another 100 (751 s), +49%. Touching llvm/IR/Function.h recompiles 58 more MLIR files (+16%), before counting CIR's lowering. Touching clang/AST/Decl.h recompiles 521 files (1,198 s) without CIR; CIRGen adds at least 51 files and 415 s, +35%. CIRGen alone costs 2.2x classic CodeGen (104 files, 189 s); in my parallel build, 1.6x (351 s vs 221 s). Memory and disk grow too: c-t-t's build max-rss is +40% (+1 GiB), which matters when running many jobs on a 16 or 32 GiB machine, and aengelke's build directory grew from 1.5 GiB to 2.4 GiB. -DCLANG_ENABLE_CIR=Off stays supported, but once premerge tests CIR, a contributor who opts out learns about CIR breakage from CI and then has to build CIR anyway. Flang shows where this goes. .ci/compute_projects.py tests flang and mlir for every llvm/ change, so fixing a breakage means building Flang and MLIR. In my full build (clang, mlir, flang, lld, …), summed per-file compile time splits clang 25.6%, mlir 19.7%, llvm 16.2%, flang 14.5%. Users who need Flang or OpenACC can get them from a vendor toolchain; a contributor whose llvm/ change breaks them has to build them to fix it. Companies that fund CIR and Flang have build farms; many volunteers and contributors from smaller companies have a desktop or a laptop, and each component that becomes everyone's responsibility raises the bar for them. Runtime, for every clang user Building CIR in costs something even without -fclangir . On c-t-t, stage2-O2-g compile time is +0.18% and max-rss +2.6%. In aengelke's dylib build, clang -emit-llvm -c empty.c executes 12% more instructions, down from +88% when clang still linked libMLIR.so , and clang -c empty.c still crashed on 2026-09-14. aengelke names dynamic relocations, global constructors, and additional cl::opt as probable causes. With -fclangir , the five worst files in the progress report took 2.0–4.1x as long, "almost all in lowering CIR to LLVM IR: 30–92 s per file, roughly split between the CIR-to-LLVM-dialect conversion and the translation to LLVM IR". A linear scan for comdats made that quadratic; #227127 cut the lowering to 8–11 s. Quadratic paths keep surfacing through the llvm dialect: #198424 for insertvalue globals, and Flang's #140268 and #209393 . For typical files, I compiled 29 random C++ files from llvm/lib and clang/lib with their build flags (-O3) and -ftime-trace , with and without -fclangir , using a Release+Asserts clang at a160747d89a3 (after #227127). -fclangir took 1.15x as long in total, 1.12x for the median file, below the progress report's 1.32x. A later run on 40 files at -O0 and -O2 (1.20x and 1.17x with assertions) found that about a third of the extra time in assertions builds is a type walk: cir::FuncType::verify ( #215922 ) walks every parameter type, and MLIR verifies types inside an assert on every FuncType::get . A release clang should be about 1.14x at -O0 and 1.12x at -O2, though it still runs the CIR verifier after CIRGen and every CIR pass, while the driver disables LLVM's verifier in release builds (about 10% of the extra). The extra time splits roughly in thirds: Phase Extra time Share of the -fclangir compile CIRGen (vs. classic IR generation) 3.6 s 4.4% CIR-to-CIR passes 3.0 s 3.6% CIR to LLVM IR (pre-lowering passes, conversion, translation) 3.8 s 4.7% optimizer and backend 0.4 s 0.5% For the median file, lowering to LLVM IR is 3.6% of the compile, and 6.5% with the CIR-to-CIR passes. The lowering row includes the pre-lowering passes ( HoistAllocas , FlattenCFG , EHABILowering , GotoSolver ); GotoSolver alone is 0.64 s, 17% of it. CIRGen's deferred emission and the module verifier run outside any -ftime-trace scope, so I attribute the untraced time to CIRGen. Lopes's AsiaLLVM 2025 report measured the incubator in June 2025. On the three CTMark programs that compiled, -fclangir took 1.9–2.9x the user time at -O0 and 1.1–1.6x at -O2. For clamr_cpuonly.cpp at -O0, classic CodeGen took 15 ms and CIRGen 289 ms, 265 ms of it in CIRGenModule::applyReplacements , which the slides call "low hanging fruit". Some costs are not measured yet. CIR emits no debug info; in Flang, #146543 cut one type's debug-info nodes from 1.75M to 8K. MLIRContext uniques types, attributes, and locations and never frees them . CIRGenFunction::getLoc eagerly calls getPresumedLoc and creates a uniqued FileLineColLoc for each call. Maintenance CIR re-implements classic CodeGen (ABI lowering, builtins, …), and CodeDuplication.md accepts "a significant amount of technical debt" kept in sync by hand. That would be true of any new IR, MLIR or not. MLIR's API churn is MLIR-specific. The 2025 keynote names it among the growing pains: "The ClangIR build sometimes gets broken by changes in MLIR or other parts of clang". I counted at least six CIR fixups since June 2025 caused by MLIR interface changes, e.g. after the RegionBranchOpInterface revamp ( #165441 ) and a MemorySlotInterfaces rename ( #229068 ). Premerge testing would turn them into every clang and mlir author's problem. Compile-time culture llvm-compile-time-tracker.com measures every LLVM commit, and regressions get bisected and discussed. I found no per-commit tracker for MLIR or Flang, and MLIR's in-tree benchmark runner mlir/utils/mbr has had no functional change since July 2022. MLIR compile-time work happens after users hit a wall: the One-Shot Dialect Conversion driver (dialect conversion was "by far the slowest part"), the LLVM dialect sharding, the MLIRIR precompiled header. Flang's #58730 , at 4x gfortran's time and memory, has been open since 2022. My reading is that MLIR's main users (machine learning compilers that compile a model once and run it many times) tolerate costs that a C++ build, recompiling large translation units on every edit, does not. Clang's bar has to be enforced on the CIR side. Reducing the cost I keep this table updated as patches land. Local numbers come from my machine (i7-14700K, 28 threads, Release with assertions, lld), with summed per-file times from .ninja_log . Patch Status Effect #221616 link only the MLIR dialects CIR uses landed 2026-09-07 ninja clang wall time +109% to +75% (PragmaTwice, 192 cores) #221813 shard the LLVM dialect's generated ops landed 2026-09-07 splits LLVMDialect.cpp.o , 121 s in aengelke's list #222576 precompiled header for MLIRIR landed 2026-09-10 c-t-t build time +58% to +47% across it #227127 cached symbol tables for comdat lookups landed 2026-10-02 lowering of the five worst files from 30–92 s to 8–11 s precompiled header for clangCIR #231068 , open CIRGen's 54 files from 312 s to 85 s summed (including 6 s for the header); user CPU 282 s to 78 s move the ceil/floor div expansion out of MLIRArithTransforms #231064 , under review clang links 16 fewer MLIR libraries (90 to 74); 64 fewer object files, 147 s summed (177 s on c-t-t) MLIROpenACCDialect without MLIRGPUDialect open ~38 s; acc declares gpu as a dependent dialect and uses its types PDL off when MLIR is only a dependency open ~21 s shard CIRDialect.cpp open 58 s locally, the slowest file in the build; its generated CIROps.cpp.inc has 102K lines Thought experiment: an MLIR-free ClangIR What does CIR take from MLIR? Dialects: cir , builtin , llvm (~900 references), acc (402), ptr (74), omp (~65), dlti . ODS: ~230 ops, 164 declarative assembly formats, 53 verifiers, 22 folders/canonicalizers. OpBuilder , symbol tables, and the region-branch, loop, call, memory-slot, and data-layout interfaces. The pass manager with 12 CIR passes, the greedy rewrite driver, and the conversion driver (205 lowering patterns, 125 with hand-written bodies). The llvm dialect and ModuleTranslation . Not used in clang's pipeline: MLIR's mem2reg and SROA (CIR implements the memory-slot interfaces, but only cir-opt tests run -mem2reg ), CSE, the inliner, the dataflow framework, and the loop dialects. The incubator's ThroughMLIR path (to affine / scf ) never went upstream; efriedma noted that "translating from ClangIR to the MLIR's loop-based dialects is probably more complicated than ClangIR itself." So for C and C++, cir is the only dialect between the AST and the llvm dialect, and the llvm dialect is a second copy of the module in LLVM IR's terms, built only to be translated. MLIR cannot skip it: ModuleTranslation finds functions and globals only as llvm dialect ops, its type translation rejects CIR types, and the OpenMP translation expects llvm dialect bodies, so a direct path would be a second ModuleTranslation written for CIR. CIRGen would port almost unchanged. Only 8.6% of its 66K lines mention mlir:: , mostly Value , Location , Type , OpBuilder , Block , and Region . An API-compatible core would keep the ~1,300 XOp::create calls, which depend only on generated signatures. The .td files are TableGen data, but they carry 818 embedded C++ blocks (521 mlir:: references) and declare MLIR interfaces, so they are reusable only with a source-compatible core. The real cost is everything else: an IR core; an ODS-equivalent emitter including the assembly-format generator (the MLIR emitters it replaces are ~14K lines, plus 5K in mlir/lib/TableGen ); the interfaces; 15K lines of passes rewritten off MLIR's drivers; and the 868 -emit-cir tests reproduced byte for byte or churned. CIR would also lose MLIR's reviewers; Lauko was nominated as maintainer for "overseeing the quality of CIR dialect (the MLIR part)". The gains: most of MLIR's ~890 s of build work, no conversion step or extra IR copy, SourceLocation as the location type, memory freed with the module, and no MLIR API churn. OpenACC forces one of three choices: drop it, duplicate Flang's acc pipeline, or keep MLIR as an optional island reached through LLVM IR metadata. Only Keane's one fallback sentence supports the third; the route (metadata, then ModuleImport , then acc ops, then the shared passes) is my extrapolation, and I found no work on it. So an MLIR-free CIR is technically plausible, but it removes the property the funders want, and I see nobody who would fund it. Its value is as a lens. It separates what only leaving MLIR fixes (immortal context uniquing, the volume of ODS-generated code, the core library closure) from what can be fixed inside MLIR. Conclusion Clang needs a better IR than the AST and the CFG, and CIR is the one that got funded. Who funds Clang has changed, and that explains much of this post. Google and Apple wrote 84% of Clang commits in 2010 and 16% in 2026. NVIDIA's, AMD's, Arm's, and Microsoft's Clang commits are mostly specific to a language mode or target, NVIDIA's mostly CIR, while the shared core is written mostly by individuals, Google, Apple, and Red Hat; Who pays for Clang has the numbers. The new funders build GPU and accelerator compilers, and their other compilers (Flang, Triton, IREE, their own stacks) are built on MLIR. For them, a Clang that emits an MLIR dialect is the point, not an implementation detail. For the C++ programmers who were Clang's main constituency, CIR has to earn its place by being better at what the AST, the CFG, and classic CodeGen do today: diagnostics, safety analyses, and optimizations that need C++ semantics, without making the compiler slower to build or run. Lifetime safety went to the CFG in 2025 rather than wait for CIR, which shows the bar is real. The decision also divides the people who ship and maintain Clang. Chrome's toolchain team expects to opt out: "If this lands, we'd have to disable the flag downstream." Red Hat's LLVM maintainer warned that "distros may still decide to disable it" and measured libclang-cpp.so growing from 76 MB to 141 MB. Android's LLVM distribution plans to configure it off, since it doesn't "want to pay any costs associated with this". The compile-time tracker now builds without CIR . If large distributors opt out, building CIR by default makes every contributor pay while many users never get a clang that can run -fclangir , and whether Clang has its second code generator depends on who built the compiler. The question was never really whether CIR should use MLIR. It is whether anyone is funded to keep the cheap path cheap. Today the measurable costs fall on Clang contributors and users, most heavily on those who build on their own machines, while the benefits of MLIR accrue to the organizations funding CIR. Tracking -fclangir on c-t-t, keeping -DCLANG_ENABLE_CIR=Off working, and landing the cheap build fixes in Reducing the cost are how the people paying the cost get something back.📝MaskRay

Saturday, October 10, 2026

Friday, October 9, 2026

Thursday, October 8, 2026

CMake: Making Hard Things EasyAt CppCon 2026, I gave a talk called “CMake: Making Hard Things Easy” about how CMake handles build-system complexity so developers can focus on their software. This article shares practical takeaways from that talk. We started CMake for ITK 26 years ago. Researchers needed to use medical image segmentation and registration algorithms across different platforms without becoming experts in every compiler and linker. The idea was to capture that expertise in the build system so everyone could benefit from it. I wrote about those early days in Happy Birthday CMake!. Today, we take it for granted that we have build tools that can create static or shared libraries and link applications to them on Linux, Windows, or macOS.📝Kitware Inc
Introducing the QML Live Preview MCP Tool for Agentic Development of Embedded DevicesAgents Edit QML, But Don’t See the Result Without Building, Compiling, and Running AI agents can make many UI changes in a single iteration. AI agents edit files, maybe run a build, and then report success. Every change can costs a full rebuild and relaunch, or it goes unchecked by the developer. Qt's QML Live Preview MCP tool makes autonomous AI agent work transparent for the developer at a low token expense: it runs the app and hot reloads changed QML files, without a rebuild and without a restart.📝Qt Blog