I tried this code (abstracted from #159302, compiler explorer):
#[inline(never)]
fn inner(_: &dyn Sync) {}
fn wrapper<T: Sync>(val: T) {
inner(&val);
}
#[inline(never)]
pub fn wrapper_u32() {
wrapper(1u32);
}
#[inline(never)]
pub fn wrapper_u32_manual(x: u32) {
inner(&x);
}
I expected to see this happen: Even without/before LLVM optimizations, the generated code for wrapper::<u32> is the same as for wrapper_u32_manual. Both just stash their argument on the stack and call inner with a pointer to that stack slot and the vtable pointer.
Instead, this happened: The code that rustc emits for wrapper::<u32> has unnecessary cleanup blocks. In the final assembly that's everything after the ret:
example[24ebc4c2b62952c4]::wrapper::<u32>:
sub rsp, 24
mov dword ptr [rsp + 4], edi
lea rsi, [rip + .Lanon.76072c6d33abe03aa460b47b2020a892.0]
lea rdi, [rsp + 4]
call example[24ebc4c2b62952c4]::inner
add rsp, 24
ret
mov qword ptr [rsp + 8], rax
mov dword ptr [rsp + 16], edx
mov rdi, qword ptr [rsp + 8]
call _Unwind_Resume@PLT
It's due to this part of the LLVM IR that rustc emits:
; ...
invoke void @example[24ebc4c2b62952c4]::inner(ptr noundef nonnull %val, ptr noalias nofree noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @vtable.0)
to label %bb1 unwind label %cleanup
bb3:
%2 = load ptr, ptr %1, align 8
%3 = getelementptr inbounds i8, ptr %1, i64 8
%4 = load i32, ptr %3, align 8
call void @llvm.lifetime.end.p0(ptr %1)
%5 = insertvalue { ptr, i32 } poison, ptr %2, 0
%6 = insertvalue { ptr, i32 } %5, i32 %4, 1
resume { ptr, i32 } %6
cleanup:
%7 = landingpad { ptr, i32 }
cleanup
%8 = extractvalue { ptr, i32 } %7, 0
%9 = extractvalue { ptr, i32 } %7, 1
call void @llvm.lifetime.start.p0(ptr %1)
store ptr %8, ptr %1, align 8
%10 = getelementptr inbounds i8, ptr %1, i64 8
store i32 %9, ptr %10, align 8
br label %bb3
Of course, LLVM optimization passes eventually clean it up, but this is probably wasting some compile time, and also hurts code size in debug builds without optimizations.
Meta
rustc --version --verbose (according to compiler explorer UI):
rustc 1.99.0-nightly (d0babd8b6 2026-07-15)
binary: rustc
commit-hash: d0babd8b6b05ef9bb65d42f928cef4129d64cf65
commit-date: 2026-07-15
host: x86_64-unknown-linux-gnu
release: 1.99.0-nightly
LLVM version: 22.1.8
Internal compiler ID: nightly
I tried this code (abstracted from #159302, compiler explorer):
I expected to see this happen: Even without/before LLVM optimizations, the generated code for
wrapper::<u32>is the same as forwrapper_u32_manual. Both just stash their argument on the stack and callinnerwith a pointer to that stack slot and the vtable pointer.Instead, this happened: The code that rustc emits for
wrapper::<u32>has unnecessary cleanup blocks. In the final assembly that's everything after theret:It's due to this part of the LLVM IR that rustc emits:
Of course, LLVM optimization passes eventually clean it up, but this is probably wasting some compile time, and also hurts code size in debug builds without optimizations.
Meta
rustc --version --verbose(according to compiler explorer UI):