Skip to content

size_of_val(p) == 0 doesn't optimize out for clearly-not-ZST values #152788

Description

@scottmcm

The following rust

pub struct Foo<T: ?Sized>(pub [u32; 3], pub T);

#[unsafe(no_mangle)]
pub fn demo(p: &Foo<dyn std::fmt::Debug>) -> bool {
    std::mem::size_of_val(p) == 0
}

currently https://rust.godbolt.org/z/r1d5n6Phe optimizes to

define noundef zeroext i1 @demo(ptr noundef nonnull readnone align 4 captures(none) %p.0, ptr noalias noundef readonly align 8 captures(none) dereferenceable(32) %p.1) unnamed_addr #0 {
start:
  %0 = getelementptr inbounds nuw i8, ptr %p.1, i64 8
  %1 = load i64, ptr %0, align 8, !range !3, !invariant.load !4
  %2 = getelementptr inbounds nuw i8, ptr %p.1, i64 16
  %3 = load i64, ptr %2, align 8, !range !5, !invariant.load !4
  %4 = tail call i64 @llvm.umax.i64(i64 %3, i64 4)
  %5 = add nuw i64 %1, 11
  %6 = add i64 %5, %4
  %7 = sub i64 0, %4
  %8 = and i64 %6, %7
  %_0 = icmp eq i64 %8, 0
  ret i1 %_0
}

but clearly that type has to be at least 12 bytes no matter what.

Why does that matter? Well, whenever you drop a Box it needs to check for ZSTs before calling the allocator. So that check should optimize out in these cases, but it doesn't today.

Filed here because we're probably not telling LLVM enough to be able to do that itself. #152786 will give it a bit more information, but probably not enough to solve this.


For some DSTs it does optimize, notably

pub fn demo_simpleslice(p: &Foo<[i32]>) -> bool {
    std::mem::size_of_val(p) == 0
}

optimizes to false.

However, this one

pub fn demo_lessalignedslice(p: &Foo<[u8]>) -> bool {
    std::mem::size_of_val(p) == 0
}

doesn't today https://rust.godbolt.org/z/nnhGKz767

define noundef zeroext i1 @demo_lessalignedslice(ptr noalias noundef readonly align 4 captures(none) dereferenceable(12) %p.0, i64 noundef %p.1) unnamed_addr #0 {
start:
  %0 = add i64 %p.1, 15
  %_0 = icmp ult i64 %0, 4
  ret i1 %_0
}

(That last one might get fixed with #152786, though, because the icmp can only be true if it wraps, which definitely can't happen.)

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 18, 2026
  2. added
    C-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing such
    A-codegenArea: Code generation
    on Feb 18, 2026
  3. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Feb 18, 2026
  4. scottmcm commented on Feb 18, 2026

    @scottmcm
    MemberAuthor

    Ideas to try:

    • Tell it the alignment is a power of two, not just that it's in 1..=isize::MIN.unsigned_abs()
    • Tell it the rounded-up size is definitely never smaller than the original size
    • Tell it the DST's size is actually a multiple of the DST's alignment
    • ...
  5. added
    A-ZSTArea: Zero-sized types (ZSTs).
    and removed
    A-ZSTArea: Zero-sized types (ZSTs).
    on Feb 18, 2026
  6. TKanX commented on Feb 19, 2026

    @TKanX
    Contributor

    @rustbot claim

  7. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Feb 23, 2026
  8. added a commit that references this issue on Mar 26, 2026
    75bcc86
  9. asquared31415 commented on Jul 27, 2026

    @asquared31415
    Contributor

    in #159921 I noticed that &Dst with a user defined ADT doesn't get range metadata on the length argument, even though slices &[T] do, and so attempted to add that range metadata to all &Dst cases. I found that it significantly regressed size_of_val calculations (due to a shortcoming of LLVM, but i'm not sure exactly if or how it should be addressed upstream)

    In particular the regression is

    pub struct Cat {
        meow: u32,
        nya: [u8],
    }
    
    #[unsafe(no_mangle)]
    pub fn ref_size_of_val_struct(x: &Cat) -> usize {
        size_of_val(&*x)
    }

    With my changes to emit range metadata on the length argument, the IR emitted with no-prepopulate-passes is:

    define noundef i64 @ref_size_of_val_struct(ptr noalias nofree noundef readonly align 4 captures(address, read_provenance) dereferenceable(4) %x.0, i64 noundef range(i64 0, 9223372036854775801) %x.1) unnamed_addr #0 {
    start:
      %0 = add i64 4, %x.1
      %1 = add i64 %0, 3
      %_0 = and i64 %1, -4
      ret i64 %_0
    }
    

    (note the range starts at a value low enough to prove that the aligned size after adding the static header will not wrap)

    with passes, LLVM "optimizes" that to

    define noundef range(i64 4, 9223372036854775805) i64 @ref_size_of_val_struct(ptr noalias nofree noundef readonly align 4 captures(none) dereferenceable(4) %x.0, i64 noundef range(i64 0, 9223372036854775801) %x.1) unnamed_addr #1 {
    start:
      %0 = add nuw nsw i64 %x.1, 7
      %_0 = and i64 %0, 9223372036854775804
      ret i64 %_0
    }
    

    The merging of adds is nice, and the nuw nsw, but that and is significantly worse, it's the same constant as -4, but with bit 63 cleared. When generating assembly code, LLVM ends up using this same constant, which on x86 is significantly worse.

    Current codegen without range metadata emits

    lea	rax, [rsi + 7]
    and	rax, -4
    ret
    

    the and is a very short instruction, only four bytes in total, using a sign extended one byte immediate.

    The codegen when adding range metadata does

    lea	rcx, [rsi + 7]
    movabs	rax, 9223372036854775804
    and	rax, rcx
    ret
    

    which uses 11 bytes for the movabs alone, since it uses a full 8 byte immediate, and it's now using rax and rcx for intermediate calculation, instead of only rax.

    The range information does allow LLVM to prove the output range, as can be seen in the optimized IR, range(i64 4, 9223372036854775805) exists on the output. This allows LLVM to optimize this specific case of checking against 0 or isize::MAX, but it may not be worth the regressions to size calculation.

  10. asquared31415 commented on Jul 27, 2026

    @asquared31415
    Contributor

    Ideally LLVM would propagate the range information on the output without modifying the constant used for the and, but I'm certain it's not so simple, this transformation is likely useful in some cases.

  11. scottmcm commented on Jul 28, 2026

    @scottmcm
    MemberAuthor

    You should file that as an LLVM issue -- if adding range metadata makes codegen worse, that's something to fix there.

    (Also be sure to try with LLVM23, hopefully coming very soon.)

    EDIT: oh, might just be #149673 aka llvm/llvm-project#196786

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

Metadata

Metadata

Assignees

Labels

A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-codegenArea: Code generationC-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing suchT-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