Is there a tidy way to query the alignment of a slice or pointer (for diagnostic purposes)?

I have this wrapper around [T]::align_to::<U>() which is used strictly in contexts where the input slice is supposed to be aligned correctly for U already. If it isn't, I'd like to print out the actual alignment of the slice for diagnostics. There doesn't seem to be a good way to do that. I found a clunky way, shown below. Have I missed something? It seems like this ought to be an intrinsic method of slices and/or raw pointers... (note: mem::align_of_val does something different)

use core::any::type_name;
use core::mem::{align_of, size_of};

pub fn view_head_as<S>(slice: &[u8]) -> &S {
    // SAFETY: In the full program S is constrained to be a type for which
    // the transmutation performed by `align_to` is safe.
    let (prefix, s_slice, _) = unsafe { slice.align_to::<S>() };

    if !prefix.is_empty() {
        // There appears to be no better way to query the actual
        // alignment of a pointer than this.  I may have missed
        // something.
        let p = slice.as_ptr();
        let mut alignment = align_of::<S>() >> 1;
        while alignment > 1 {
            if p.align_offset(alignment) == 0 {
                break;
            }
            alignment >>= 1;
        }
        panic!(
            "slice not properly aligned for {}: {} < {}",
            type_name::<S>(),
            alignment,
            align_of::<S>()
        );
    }

    s_slice.get(0).unwrap_or_else(|| {
        panic!(
            "slice too small to view as {}: {} < {}",
            type_name::<S>(),
            slice.len(),
            size_of::<S>()
        )
    })
}

(Playground)

// <ptr>::addr is only stable in 1.84
// You can replace it with `ptr as usize`
pub fn alignment<T>(ptr: *const T) -> usize {
    1 << ptr.addr().trailing_zeros()
}

(Playground)

Perfect, thank you. I always forget about trailing_zeros().

Beware that 1_usize << usize::BITS gives 1, so that may or may not be what you want.

Consider restricting it to NonNull<T> instead of *const T, so you don't need to worry about null pointers, which are arguable "infinitely-aligned".

(As a bonus, NonZeroUsize::trailing_zeros gets slightly nicer codegen on the default x64 target than usize::trailing_zeros.)

3 Likes

Hm. The actual thing I want the alignment of is always the referent of a &[u8]. Experimenting with variations on @cod10129's suggestion...

use std::ptr;
use std::ptr::NonNull;

#[inline(never)]
pub fn alignment_ptr(p: *const u8) -> usize {
    1 << p.addr().trailing_zeros()
}

#[inline(never)]
pub fn alignment_ref_1(r: &u8) -> usize {
    1 << ptr::from_ref(r).addr().trailing_zeros()
}

#[inline(never)]
pub fn alignment_ref_2(r: &u8) -> usize {
    1 << NonNull::from(r).addr().trailing_zeros()
}

#[inline(never)]
pub fn alignment_slice_1(s: &[u8]) -> usize {
    1 << s.as_ptr().addr().trailing_zeros()
}

#[inline(never)]
pub fn alignment_slice_2(s: &[u8]) -> usize {
    1 << NonNull::from(s).addr().trailing_zeros()
}

(#[inline(never)] is just to force generation of assembly) stable produces this x86 assembly for the first option:

	testq	%rdi, %rdi
	je	.LBB0_1
	rep		bsfq	%rdi, %rcx
	movl	$1, %eax
	shlq	%cl, %rax
	retq

.LBB0_1:
	movl	$64, %ecx
	movl	$1, %eax
	shlq	%cl, %rax
	retq

and this assembly for all four other options:

	movq	%rdi, %rax
	negq	%rax
	andq	%rdi, %rax
	retq

So clearly it is a good idea to use some construct that lets the compiler take advantage of references being guaranteed to be non-null, but, starting from an actual reference, it seems unnecessary to bring in NonNull. (It is, perhaps, regrettable that ptr::from_ref doesn't return a NonNull, but I imagine it would be very hard to change that now.)

Curiously, as you can see from the playground, the compiler knows that it has produced the same assembly for alignment_ref_1 and alignment_ref_2, and it knows that it has produced the same assembly for alignment_slice_1 and alignment_slice,_2, but it doesn't know that it has produced the same assembly for alignment_slice_1 as for alignment_ref_1.

1 Like

That's because the LLVM pass is function merging, and the functions have different signatures.

Something like BOLT can probably merge the assembly representations, though.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.