Repository navigation
Description
When compiling image@0.25.10 without default features we expected that to be a rather quick affair of 7 additional dependencies, the slowest being num-traits with a build dependency. Instead, it's slow enough for issues being opened in our own issue tracker.
We investigated further and, with perf, found ~12% of build time spent in consteval of which almost all samples fell into InterpCx::layout_of queries. That was curious since we use a few const tables but definitely not of extreme size nor complicated computation and really only with primitive types. Through instrumenting a custom compiler build (printf debugging the types being queried), we found this pattern at the top of the summary:
RDTSC count type
-------
2968652 48376 u8
2975486 75647 usize
3372234 2024 std::num::NonZero<usize>
3494159 65639 FnDef(DefId(2:705 ~ core[93c5]::f64::{impl#0}::to_bits), [])
6016422 107343 ()
7589954 145971 FnDef(DefId(21:947 ~ pxfm[d8cc]::common::fmla), [])
10072816 175205 i64
15194244 290049 i32
22053787 392617 u32
22284234 322397 bool
40392452 615785 u64
58157921 2110 std::alloc::Layout
179631693 2996946 f64
Since there are far fewer than 2 million (monomorphized) lines in the whole dependency tree, this points to layouts being evaluated highly redundantly—once per evaluation of at least some statements / expression parts rather than what we'd rather want once per unique definition in MIR.
Meta
rustc --version --verbose:
rustc 1.95.0 (59807616e 2026-04-14)
binary: rustc
commit-hash: 59807616e1fa2540724bfbac14d7976d7e4a3860
commit-date: 2026-04-14
host: x86_64-unknown-linux-gnu
release: 1.95.0
LLVM version: 22.1.2
Development History
In PR #156718 I tried the idea of making the queries themselves faster by special-casing primitive types, which have known layouts that can be pre-interned thus replacing a function call with a few moves. This did yield a 2% but ultimately a weird case to make when the problematic behavior is the repetition of queries themselves.
Further instrumentation with the source of each layout call revealed that the vast majority was coming from InterpCx::layout_of_local which is itself more curious due to locals already having a cache attached to it per local that apparently is not hit, most of the time. As pointed out (#156718 (comment)) the design would explicitly not work for small const functions called multiple times due to it being attached to the frame of a call, not more directly to a MIR body.
count source:type
------
164879 layout_of_local:i64
227240 layout_of_local:bool
270325 layout_of_local:u32
579206 layout_of_local:u64
2860447 layout_of_local:f64
Activity
theemathas commentedon May 27, 2026
Possibly related to #148335?
cc @oli-obk
RalfJung commentedon May 27, 2026
To be clear, it's not evaluated that often, it's just requested that often. Almost all of these should hit the cache.
Cc @nnethercote
Do you know what percentage of time is spent inside
layout_of_uncached? That's the actual time spent computing layout (as opposed to looking it up from the cache).[-]Consteval caching is surprisingly ineffective[/-][+]Consteval layout_of caching is surprisingly ineffective[/+]oli-obk commentedon May 27, 2026
We could have a fast path wrapper around the query that uses a match to load preinterned layouts of primitives. That's def faster than hashing and looking it up
Auto merge of #157275 - RalfJung:interpret-layout-cache, r=oli-obk
Auto merge of #157275 - RalfJung:interpret-layout-cache, r=oli-obk
Auto merge of #157275 - RalfJung:interpret-layout-cache, r=oli-obk
197g commentedon Aug 8, 2026
The same is not true for functions. I've added instrumentation to add the callstack for each query and just ran the compilation process again. In
moxcms@0.8.1there is this pattern generating some constant LUT tables each of them a call to const-eval functionsColorProfile::colorants_matrix(X, Y). These separate entry point in turn call to constant matrix multiplication and other math, e.g. repeatedly runningpxfm::floor::floorwhich does ~20 queries forf64. Many of these queries are apparently not cached and the callstack contains a lot of basic blocks fromf64::{impl#?}::*functions. In total this works out to hundred of thousands of layout queries for theu64andf64type alone until the constant tables have been calculated.So when I'm tracing all calls to
layout_ofmade during evaluation (tyand stack frame), these three queries appear thousands of time sincef64::from_bitsis called thousands of times:The function body of
from_bitsis of course:The transmute is MIR-optimized into an inline op as such:
But I think every type and layout involved could be preserved from prior calls to this monomorphic function, and associated to the MIR blocks somehow. The types here never change and this body is evaluated
> 10ktimes in this crate. Similar patterns show many other occurrences of basic manipulations that would be assumed to be cheap if they were at runtime.The
std::alloc::Layouttiming is on closer inspection an artifact with two big outliers (it seems suspicious this would show up on this specific type and they happen whenalignment::AlignmentEnum::*is constant evaluated…) but still each reference tocore::mem::SizedTypeProperties::ALIGN, while individually for unique type instantiations, does more 100 queries while it steps through the type-independent monomorphiccore::layout::from_size_align_uncheckedcall in its body and its various subroutines in the relatedprecondition_check. Most of these queries are forusize, again and again. That is,Arc::neworBox::newis surprisingly expensive and so is thevec!macro use.RalfJung commentedon Aug 8, 2026
I don't understand what you are trying to say. It is not clear to me what exactly you measured and what you saw.
The queries happening is expected. They will just return a result from a cache immediately, they won't do any expensive computations. Since #157275 we have an extra layer of caching directly in the interpreter so the query happens at most once per interpreter (but if you have many small constants that won't do anything, it only helps for when a single constant evaluation needs the same layout again and again).
Also, which query do you even mean? It's entirely unclear what you are debug-printing here.
197g commentedon Aug 10, 2026
I should have used the standard
FrameInfo/generate_stacktrace_from_stack. Corrected above, this is a complete trace of alllayout_ofcalls made during const-eval while compiling the crates. It shows that in total, this one table constant is responsible for about a fifth of the queries (1118362 of 5747066) in that trace. Most of those are the repeated queries to type the MIR forf64::from_bitsand equivalent functions.As for expectations, it's true that it fits with the current implementation. However, I disagree that this implementation is expected. There's a lot of rather small
const fnabstractions, good practice I'd say, and with stack frames each argument and return value is already a query tolayout_offor every call made. In these cases that's probably 16 times the type query overhead compared to inlining all those functions so that their types are cachedlocalsof the basic block in the loop. This is directly observable. Compare the following two programs (N chosen to be in the ballpark of the above type query count):Inlined float manipulation
Outlined floating point math
Local testing (cargo 1.99.0-nightly (b07e5a086 2026-08-07)) shows them to compile in
0m0,518svs.0m0,822srespectively. Furthermore, in my sample the second crate spends roughly 15 times more time performing roughly 15 times more type queries (from 262_243 to 3_932_352; and 84ms). I'm sure there's also other costly overhead by the function indirection but this one seems unnecessary at face value. The MIR ofupdate_sumdoes not change and neither should the types and layouts assigned to any of its expressions in each call frame anew.instrument.diff.txt
RalfJung commentedon Aug 10, 2026
RalfJung commentedon Aug 10, 2026