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.