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

If this page is useful, please consider donating a coffee

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
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.📝MaskRay
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

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

Wednesday, October 7, 2026

Tuesday, October 6, 2026