Skip to content

Consteval layout_of caching is surprisingly ineffective #157010

Open

Description

@197g

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

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on May 27, 2026
  2. added
    I-compiletimeIssue: Problems and improvements with respect to compile times.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)
    A-layoutArea: Memory layout of types
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on May 27, 2026
  3. theemathas commented on May 27, 2026

    @theemathas
    Contributor

    Possibly related to #148335?

    cc @oli-obk

  4. RalfJung commented on May 27, 2026

    @RalfJung
    Member

    this points to layouts being evaluated highly redundantly

    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).

  5. changed the title [-]Consteval caching is surprisingly ineffective[/-] [+]Consteval layout_of caching is surprisingly ineffective[/+] on May 27, 2026
  6. oli-obk commented on May 27, 2026

    @oli-obk
    Contributor

    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

  7. added 2 commits that reference this issue on Jun 6, 2026
  8. 197g commented on Aug 8, 2026

    @197g
    ContributorAuthor

    To be clear, it's not evaluated that often, it's just requested that often.

    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.1 there is this pattern generating some constant LUT tables each of them a call to const-eval functions ColorProfile::colorants_matrix(X, Y). These separate entry point in turn call to constant matrix multiplication and other math, e.g. repeatedly running pxfm::floor::floor which does ~20 queries for f64. Many of these queries are apparently not cached and the callstack contains a lot of basic blocks from f64::{impl#?}::* functions. In total this works out to hundred of thousands of layout queries for the u64 and f64 type alone until the constant tables have been calculated.

    So when I'm tracing all calls to layout_of made during evaluation (ty and stack frame), these three queries appear thousands of time since f64::from_bits is called thousands of times:

    u64     [FrameInfo { instance: Instance { def: Item(DefId(2:708 ~ core[6b2d]::f64::{impl#0}::from_bits)), args: [] }, span: library/core/src/num/f64.rs:1308:28: 1308:29 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(21:960 ~ pxfm[eada]::common::copysignk)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pxfm-0.1.19/src/common.rs:232:5: 232:75 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2300 ~ moxcms[156c]::defaults::hlg_curve)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:175:5: 175:25 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2296 ~ moxcms[156c]::defaults::build_trc_table_hlg)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:138:22: 138:34 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2307 ~ moxcms[156c]::defaults::HLG_LUT_TABLE)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:181:40: 181:61 (#0) }]
    u64     [FrameInfo { instance: Instance { def: Item(DefId(2:708 ~ core[6b2d]::f64::{impl#0}::from_bits)), args: [] }, span: library/core/src/num/f64.rs:1311:33: 1311:34 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(21:960 ~ pxfm[eada]::common::copysignk)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pxfm-0.1.19/src/common.rs:232:5: 232:75 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2300 ~ moxcms[156c]::defaults::hlg_curve)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:175:5: 175:25 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2296 ~ moxcms[156c]::defaults::build_trc_table_hlg)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:138:22: 138:34 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2307 ~ moxcms[156c]::defaults::HLG_LUT_TABLE)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:181:40: 181:61 (#0) }]
    f64     [FrameInfo { instance: Instance { def: Item(DefId(2:708 ~ core[6b2d]::f64::{impl#0}::from_bits)), args: [] }, span: library/core/src/num/f64.rs:1311:18: 1311:35 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(21:960 ~ pxfm[eada]::common::copysignk)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/pxfm-0.1.19/src/common.rs:232:5: 232:75 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2300 ~ moxcms[156c]::defaults::hlg_curve)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:175:5: 175:25 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2296 ~ moxcms[156c]::defaults::build_trc_table_hlg)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:138:22: 138:34 (#0) }, FrameInfo { instance: Instance { def: Item(DefId(0:2307 ~ moxcms[156c]::defaults::HLG_LUT_TABLE)), args: [] }, span: //.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/moxcms-0.8.1/src/defaults.rs:181:40: 181:61 (#0) }]
    

    The function body of from_bits is of course:

    pub const fn from_bits(v: u64) -> Self {
        unsafe { mem::transmute(v) }
    }
    

    The transmute is MIR-optimized into an inline op as such:

        let mut _0: f64;
    
        bb0: {
            _0 = copy _1 as f64 (Transmute);
            return;
        }
    

    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 > 10k times 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::Layout timing is on closer inspection an artifact with two big outliers (it seems suspicious this would show up on this specific type and they happen when alignment::AlignmentEnum::* is constant evaluated…) but still each reference to core::mem::SizedTypeProperties::ALIGN, while individually for unique type instantiations, does more 100 queries while it steps through the type-independent monomorphic core::layout::from_size_align_unchecked call in its body and its various subroutines in the related precondition_check. Most of these queries are for usize, again and again. That is, Arc::new or Box::new is surprisingly expensive and so is the vec! macro use.

  9. RalfJung commented on Aug 8, 2026

    @RalfJung
    Member

    I don't understand what you are trying to say. It is not clear to me what exactly you measured and what you saw.

    For instance, these three queries happen in a leaf of the callstack in one of thousands of f64::from_bits evals:

    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.

  10. 197g commented on Aug 10, 2026

    @197g
    ContributorAuthor

    I should have used the standard FrameInfo/generate_stacktrace_from_stack. Corrected above, this is a complete trace of all layout_of calls 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 for f64::from_bits and 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 fn abstractions, good practice I'd say, and with stack frames each argument and return value is already a query to layout_of for 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 cached locals of 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
    const N: usize = 1 << 18;
    
    const CONSTANT: [f32; N] = {
        let mut v = [0.0; N];
        let mut i = 0;
        let mut sum = 0.0;
    
        loop {
            i += 1;
    
            if i >= N {
                break;
            }
    
            v[i] = {
                let v = i as f64;
                sum += 1.0 / v;
                sum as f32
            };
        }
    
        v
    };
    
    fn main() {
        for (i, &v) in CONSTANT.iter().enumerate() {
            println!("{}", v - (i as f32).ln());
        }
    }
    Outlined floating point math
    const N: usize = 1 << 18;
    
    const CONSTANT: [f32; N] = {
        let mut v = [0.0; N];
        let mut i = 0;
        let mut sum = 0.0;
    
        loop {
            i += 1;
    
            if i >= N {
                break;
            }
    
            v[i] = update_sum(&mut sum, i);
        }
    
        v
    };
    
    const fn update_sum(sum: &mut f64, i: usize) -> f32 {
        let v = i as f64;
        *sum += 1.0 / v;
        *sum as f32
    }
    
    fn main() {
        for (i, &v) in CONSTANT.iter().enumerate() {
            println!("{}", v - (i as f32).ln());
        }
    }

    Local testing (cargo 1.99.0-nightly (b07e5a086 2026-08-07)) shows them to compile in 0m0,518s vs. 0m0,822s respectively. 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 of update_sum does not change and neither should the types and layouts assigned to any of its expressions in each call frame anew.

    instrument.diff.txt

  11. RalfJung commented on Aug 10, 2026

    @RalfJung
    Member
  12. RalfJung commented on Aug 10, 2026

    @RalfJung
    Member
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-const-evalArea: Constant evaluation, covers all const contexts (static, const fn, ...)A-layoutArea: Memory layout of typesC-bugCategory: This is a bug.I-compiletimeIssue: Problems and improvements with respect to compile times.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions