Skip to content

Performance regression since 1.93 related to MaybeUninit being wrongfully initialized #157743

Closed

Description

@glandium

Since rustc 1.93 (likely #147827), initializing newtype wrapper of a struct that contains an integer type and a MaybeUninit field generates code to initialize the entire struct at -O2, instead of writing only the initialized integer field.

First bad nightly: nightly-2025-11-24 (commit c23ed3e)
Last good nightly: nightly-2025-11-23 (commit 94b49fd)
Still broken as of: 1.98.0-nightly (2026-06-11, LLVM 22.1.6)

Real-world impact: this caused a 22% performance regression in a specific test in the Firefox performance suite, as reported in https://bugzilla.mozilla.org/show_bug.cgi?id=2046376.
The hot call site creates a struct of three empty SmallVecs (from the smallvec crate with the union feature enabled). With rustc 1.93 the hot path now calls memset for 1224-bytes instead of three 8-byte stores.

Minimal reproducer

use std::mem::MaybeUninit;

pub struct Vec { cap: usize, data: MaybeUninit<[u64; 2]> }
pub struct Wrap(Vec);

#[no_mangle]
#[inline(never)]
pub fn make() -> Wrap {
    Wrap(Vec { cap: 0, data: MaybeUninit::uninit() })
}

https://godbolt.org/z/KnarEEon8

1.92 generates:

make:
        movq    %rdi, %rax
        movq    $0, 16(%rdi)
        retq

1.93 generates:

make:
        movq    %rdi, %rax
        xorps   %xmm0, %xmm0
        movups  %xmm0, (%rdi)
        movq    $0, 16(%rdi)
        retq

With larger arrays in the MaybeUninit, more movups happen, up to the point where it may choose to use memset instead.
IOW, 19.3 is initializing the whole struct instead. Without the wrapping, the MaybeUninit is properly not initialized.

Activity

  1. added
    C-bugCategory: This is a bug.
    regression-untriagedUntriaged performance or correctness regression.
    on Jun 11, 2026
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 11, 2026
  3. scottmcm commented on Jun 11, 2026

    @scottmcm
    Member

    LLVM emitted by rustc: https://godbolt.org/z/EKh7c671v

    1.92

    define void @make(ptr dead_on_unwind noalias noundef writable sret([24 x i8]) align 8 captures(address) dereferenceable(24) %_0) unnamed_addr {
    start:
      %_2 = alloca [16 x i8], align 8
      %_1 = alloca [24 x i8], align 8
      call void @llvm.lifetime.start.p0(i64 24, ptr %_1)
      call void @llvm.lifetime.start.p0(i64 16, ptr %_2)
      %0 = getelementptr inbounds i8, ptr %_1, i64 16
      store i64 0, ptr %0, align 8
      call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_1, ptr align 8 %_2, i64 16, i1 false)
      call void @llvm.lifetime.end.p0(i64 16, ptr %_2)
      call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_0, ptr align 8 %_1, i64 24, i1 false)
      call void @llvm.lifetime.end.p0(i64 24, ptr %_1)
      ret void
    }

    1.93 (and nightly)

    @anon.099dafc678df7d266c25f95ccf6cde22.0 = private unnamed_addr constant [16 x i8] undef, align 8
    
    define void @make(ptr dead_on_unwind noalias noundef writable sret([24 x i8]) align 8 captures(none) dereferenceable(24) %_0) unnamed_addr {
    start:
      %_1 = alloca [24 x i8], align 8
      call void @llvm.lifetime.start.p0(i64 24, ptr %_1)
      %0 = getelementptr inbounds i8, ptr %_1, i64 16
      store i64 0, ptr %0, align 8
      call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_1, ptr align 8 @anon.099dafc678df7d266c25f95ccf6cde22.0, i64 16, i1 false)
      call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_0, ptr align 8 %_1, i64 24, i1 false)
      call void @llvm.lifetime.end.p0(i64 24, ptr %_1)
      ret void
    }

    Here's where LLVM's MemCpyOpt changes the 16-byte memcpy of undef plus a 8-bytes store into a 24-byte memset: https://godbolt.org/z/WWvvxhE4n

  4. scottmcm commented on Jun 11, 2026

    @scottmcm
    Member

    Oh, found it: I think this is a dup of #152541 which is marked as fixed in upstream LLVM by llvm/llvm-project#181609

  5. glandium commented on Jun 11, 2026

    @glandium
    ContributorAuthor

    Is there any chance this will be fixed/worked around in rustc before waiting for the next LLVM release being picked up?

  6. zachs18 commented on Jun 11, 2026

    @zachs18
    Contributor

    Bisected to nightly-2025-11-24 which included #147827, as noted in OP. CI artifacts from they aren't available so couldn't easily bisect further. Also, -Zmir-enable-passes=-GVN gives the "good" asm again, which also points to #147827

    On the LLVM side, it seems like the MemCpyOptPass pass is the main issue; at that point in the opt pipeline, the IR is basically memcpy(&mut dest.data, const { &MaybeUninit::uninit() }); dest.cap = 0;, and MemCpyOptPass seems to change it into memset(&mut dest, 0): (godbolt link, see "Opt Pipeline" view)

    // before MemCpyOptPass pass
    @0 = private unnamed_addr constant [16 x i8] undef, align 8
    
    define void @example[9407090cffdc3c0d]::make(ptr dead_on_unwind noalias nofree noundef writable writeonly sret([24 x i8]) align 8 captures(none) dereferenceable(24) %_0) unnamed_addr {
    start:
      tail call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 8 dereferenceable(16) %_0, ptr noundef nonnull align 8 dereferenceable(16) @0, i64 16, i1 false)
      %_1.sroa.4.0._0.sroa_idx = getelementptr inbounds nuw i8, ptr %_0, i64 16
      store i64 0, ptr %_1.sroa.4.0._0.sroa_idx, align 8
      ret void
    }
    // after MemCpyOptPass pass
    @0 = private unnamed_addr constant [16 x i8] undef, align 8
    
    define void @example[9407090cffdc3c0d]::make(ptr dead_on_unwind noalias nofree noundef writable writeonly sret([24 x i8]) align 8 captures(none) dereferenceable(24) %_0) unnamed_addr {
    start:
      %_1.sroa.4.0._0.sroa_idx = getelementptr inbounds nuw i8, ptr %_0, i64 16 // this is removed by a later InstCombine pass
      call void @llvm.memset.p0.i64(ptr align 8 %_0, i8 0, i64 24, i1 false)
      ret void
    }

    For comparison, the IR at this stage in the LLVM pipeline with -Zmir-enable-passes=-GVN (I think):

    define void @example[9407090cffdc3c0d]::make(ptr dead_on_unwind noalias nofree noundef writable writeonly sret([24 x i8]) align 8 captures(none) dereferenceable(24) %_0) unnamed_addr {
    start:
      %_1.sroa.4.0._0.sroa_idx = getelementptr inbounds nuw i8, ptr %_0, i64 16
      store i64 0, ptr %_1.sroa.4.0._0.sroa_idx, align 8
      ret void
    }

    Maybe LLVM treats constant undef differently from an uninitialized alloca? The unoptimized LLVM IR that rustc emits in the "bad" version has a @anon.05d519269bd06692ed4427bd5298e9b5.0 = private unnamed_addr constant [16 x i8] undef, align 8 that it memcpys from, whereas the "good" version without GVN instead copies from an uninitialized alloca (godbolt link comparison). edit: see here

    cargo bisect-rustc output
    // src/lib.rs
    use std::mem::MaybeUninit;
    
    pub struct Vec { cap: usize, data: MaybeUninit<[u64; 2]> }
    pub struct Wrap(Vec);
    
    #[inline(never)]
    pub fn make() -> Wrap {
        Wrap(Vec { cap: 0, data: MaybeUninit::uninit() })
    }
    $ cargo install cargo-show-asm
    // ...
    $ cargo bisect-rustc --preserve --prompt --start 1.92.0 --end 1.93.0 --without-cargo -- asm
    // respond "baseline" on the good asm and "regressed" on the regressed asm
    searched toolchains nightly-2025-10-24 through nightly-2025-12-05
    
    
    ********************************************************************************
    Regression in nightly-2025-11-24
    ********************************************************************************
    
    fetching https://static.rust-lang.org/dist/2025-11-23/channel-rust-nightly-git-commit-hash.txt
    nightly manifest 2025-11-23: 40 B / 40 B [=====================================================================================================================] 100.00 % 1.18 MB/s converted 2025-11-23 to 94b49fd998d6723e0a9240a7cff5f9df37b84dd8
    fetching https://static.rust-lang.org/dist/2025-11-24/channel-rust-nightly-git-commit-hash.txt
    nightly manifest 2025-11-24: 40 B / 40 B [=====================================================================================================================] 100.00 % 9.73 KB/s converted 2025-11-24 to c23ed3ef28b3cc30658310f771ddc66e5b687cf4
    looking for regression commit between 2025-11-23 and 2025-11-24
    fetching (via remote github) commits from max(94b49fd998d6723e0a9240a7cff5f9df37b84dd8, 2025-11-21) to c23ed3ef28b3cc30658310f771ddc66e5b687cf4
    ending github query because we found starting sha: 94b49fd998d6723e0a9240a7cff5f9df37b84dd8
    get_commits_between returning commits, len: 7
      commit[0] 2025-11-22: Auto merge of #149222 - matthiaskrgr:rollup-cl428i7, r=matthiaskrgr
      commit[1] 2025-11-23: Auto merge of #149096 - chenyukang:yukang-fix-unused-vars-148373, r=oli-obk
      commit[2] 2025-11-23: Auto merge of #148882 - 20jasper:patch-1, r=nnethercote,Kivooeo
      commit[3] 2025-11-23: Auto merge of #149170 - ZuseZ4:automate-offload-packager, r=oli-obk
      commit[4] 2025-11-23: Auto merge of #147804 - tmiasko:move-copy, r=cjgillot,saethlin
      commit[5] 2025-11-23: Auto merge of #147827 - saethlin:maybeuninit-codegen2, r=scottmcm
      commit[6] 2025-11-23: Auto merge of #148925 - madsmtm:jemalloc-perf, r=Kobzol
    ERROR: no CI builds available between 94b49fd998d6723e0a9240a7cff5f9df37b84dd8 and c23ed3ef28b3cc30658310f771ddc66e5b687cf4 within last 167 days
  7. removed
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 11, 2026
  8. rustbot commented on Jun 11, 2026

    @rustbot
    Collaborator

    Assigning P-high (discussion on Zulip).

  9. added
    llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes
    regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    and removed
    regression-untriagedUntriaged performance or correctness regression.
    on Jun 11, 2026
  10. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jun 11, 2026
  11. added a commit that references this issue on Jul 22, 2026
    e7795af
  12. added a commit that references this issue on Jul 23, 2026
    b7a3663
  13. added a commit that references this issue on Jul 23, 2026
    787ade8
  14. added a commit that references this issue on Jul 28, 2026
  15. added a commit that references this issue on Jul 28, 2026
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-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.P-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixesregression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions