My first month working on Rust compiler performance for Hexcat is over and it’s time to see some of what’s happened since my last post.

Spans

Spans are a critical data structure in the Rust compiler. Every AST, HIR, and MIR node carries a span. Each span has four fields recording the following.

  • A source location (lo and hi). A crate’s source files are concatenated into a single buffer, and these are indices into that buffer.
  • Possibly some hygiene and provenance data (context), relevant for macros and other things.
  • Possibly an extra relative position (parent) that is used to minimize the compile time impact of small changes to source locations, such as those caused by inserting a single line at the start of a file.

Each of the four fields is 32 bits, resulting in 16 bytes of data. But most of the time these 16 bytes can be compressed into one of three 8-byte inline formats, and the remaining cases are interned into a global table. This works by taking advantage of patterns in the data: many source locations have a small length (i.e. a small difference between lo and hi); many spans have a zero context and/or parent, and the non-zero contexts and parents are typically small integers; parents are only used in incremental builds.

#163857: In this PR I replace the old compression scheme with one that is simpler and more efficient. For incremental builds we now typically intern around 1% of spans compared to 20-40% with the old scheme. (In non-incremental builds we typically intern fewer than 0.1%.) Also, we can always store the context field inline even in the interned case, which is useful because we often read the context by itself. This change reduced instruction counts across many benchmarks, in the best cases by more than 3%, and the cycle count improvements were even higher. Incremental builds saw the largest improvements due to less interning.

#163971: In this PR Josh Triplett took advantage of the new representation to micro-optimize a number of span operations. This reduced instruction counts across many benchmarks, in the best case by more than 2%.

#164291: In this PR I tried using a pattern type to give Span a niche so that Option<Span> would be the same size as Span. It worked but didn’t help performance because we don’t use Option<Span> much, and it made the code more complicated. So I abandoned it.

#163628: During metadata encoding/decoding and stable hashing we typically need to get information about the same parent span from a hash table over and over. In this PR I put a single-entry cache in front of that hash table, which avoids the hash table lookup in most cases. This reduced instruction counts on many benchmarks, in the best case by more than 4%, mostly benefiting incremental compilation.

New trait solver

#163981, #164234, #165160: In these three PRs I focused on the compilation time of Diesel, a crate for which the in-development new trait solver is slower than the old trait solver. I did a dozen or so micro-optimizations across the three PRs, such as early returns on hot paths, avoiding allocations, avoiding some recomputations, and optimizing some special cases. These reduced the instruction count overhead of the new solver on a Check Full build from 1.49x to 1.16x, and other new solver benchmarks like typenum and nalgebra also saw improvements. There is still more work to be done but this is good progress.

Miscellaneous

#164236: In this PR I rearranged some code to avoid doing some unnecessary work in an early return case, reducing instruction counts for many benchmarks, in the best case by over 4%.

#165022: In this PR I removed the AttrTokenStream and AttrTokenTree types. These mirrored the widely-used TokenStream and TokenTree types but were only used for cfg processing. This change was mostly about making the code simpler, but it also avoided some token stream conversions, reducing instruction counts on a range of benchmarks: by 4% on the deep-vector stress test, and by less than 1% on some real world cases.

#164965: In this PR I avoided the creation of one type variable per element in literals requiring type inference like vec![1, 2, 3, 4]. This was a huge win (8-12% cycle count reduction) for the deep-vector stress test containing a vec literal with more than 100,000 elements… and didn’t help with any other benchmarks. Sometimes big wins on stress tests translate to smaller wins on real code, and sometimes they don’t. I ended up merging this change anyway because it was very simple and did improve an error message in one test.

#164341: In this PR I added some additional cases to an early return when processing coercions. This reduced instruction counts across numerous benchmarks, mostly by less than 1%.

#165112: There is an existing trivial_consts mechanism in the compiler that skips entire passes (including borrow checking) for trivial consts like const N: usize = 0, to reduce compile times. In this draft PR I have extended this mechanism so it also avoids building MIR for these consts. In the best case (libc) it reduces instruction counts by up to 6%, but trivial_consts is already tricky and somewhat controversial, so the path forward here is unclear. More details are in the Zulip thread.

#164484: In this PR Takayuki Maeda introduced a sparse encoding mechanism for metadata. This increased instruction counts slightly, but cycle counts and wall-times were neutral, and more importantly it reduced metadata sizes by up to 37% and max RSS (peak memory) by up to 7%.

Hexcatconf

In October I also travelled to Delft to attend Hexcatconf, a small meetup featuring twenty-ish people from Hexcat, RustNL, and elsewhere. A long trip, but well worth it! It was great to catch up with some familiar faces, make some new friends, and learn more about some of the things people are working on. Thank you to Mara Bos for organizing the event.

Overall progress

The wall-time measurements for the period 2026-09-28 to 2026-11-02 that meet the “relevant” threshold can be seen here. It’s a sea of green, with improvements on 236 tests across 45 benchmarks, a number of them being double-digit percentages, and regressions on just four tests across three benchmarks.

If we expand the selection to include “non-relevant” results (i.e. those below the variance thresholds) the results are slightly more mixed (599 improvements vs. 90 regressions) which is to be expected for the relatively short period of one month. Overall we see a very healthy mean wall-time reduction of 2.85%, and an even better mean max-RSS (peak memory) reduction of 4.46%. Good news, which hopefully will continue.

Finally, the instruction count changes since the start of the year show something remarkable: out of 561 measurements there are 560 improvements and a single regression, and the second-worst improvement is 3.10%. This observation came from Matyáš Racek; in fact, when he mentioned it a few weeks ago every single result was green. It is fantastic to see the compiler and related tools continually getting faster even as new features are added to the language and toolchain. Well done to the many people who have contributed to this achievement. Let’s keep going!