I64,u64 offset 12 byte instead of 8 byte

fn main() {

    println!("i32 size:{}", std::mem::size_of::<i32>());
    println!("i64 size:{}", std::mem::size_of::<i64>());
    println!("isize size:{}", std::mem::size_of::<isize>());




    let a:i64 = 22;
    let b = 33;


    println!("a addr:{:p}",&a);
    println!("b addr:{:p}",&b);



    let p1 = &a as *const i64;
    let p1 = p1 as *const u8;

    let p2 = &b as *const i32;
    let p2 = p2 as *const u8;

    let t = p2 as isize - p1 as isize;

    println!("p2-p1 addr: {}",t);

    unsafe {
       for elem in 0..t {
        println!("{}: {}",elem, *p1.offset(elem));
       }
    }

}

print

i32 size:4
i64 size:8
isize size:8
a addr:0x7ffc6a45d820
b addr:0x7ffc6a45d82c
p2-p1 addr: 12
0: 22
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
7: 0
8: 48
9: 217
10: 69
11: 106

Nothing says independent variables have to be consecutive in memory, or in any particular order even; they just have to respect their layout, including alignment.

If you want an explanation which can absolutely not be counted on to hold generally, i64 has an alignment of 8 and i32 has an alignment of 4. You may be seeing padding bytes between the two. (Or maybe some non-padding data.) I ran your example in the playground but swapped the order of variable declaration, and the difference was 4 instead.

Again, this behavior can absolutely not be counted on. If you need a specific layout between multiple variables, you're going to need to put them in a struct annotated with #[repr(C)] and/or friends.

5 Likes

Whether it is 4-byte alignment or 8-byte alignment, a and b should be consecutive to meet expectations. It is strange that i32 meets expectations and i64 cannot meet expectations.

In C, performing comparison/arithmetics with two pointer values came from different allocations(either stack or heap, the C spec calls it object) is UB. In Rust it's not an UB at least since you can do so within safe context, but the result of such operation is not specified.

3 Likes

My expectations are that the compiler is able to optimize a lot of things, including local variables, which clearly is against your expectations.

What's strange here is that i32 meet your expectation, not the opposite.

2 Likes

The compiler makes absolutely no guarantees about where local variables will be placed on the stack. So you cannot have any expectations other than that some random addresses will be printed to the screen.

As an example, the compiler may see that a only ever holds the value 22 so it uses an optimisation like rvalue static promotion to turn it into a static variable, so &a would then point into your program image's .rodata section.

It could also realise that some stack space can be saved by reordering a, b, t, and other values (e.g. the stack pointer and return pointer) placed on the stack.

The compiler might also see that comparing the address of two distinct objects on the stack makes no sense (you can only do pointer arithmetic using pointers within the same allocation), and decide to "optimise" the difference to the constant, 42.

All of these optimisations would apply in C or C++ as well, and are completely legal based on the respective language specs.

7 Likes

Says who? That's not how it works. Distinct variables are not required to have any particular layout in memory (they might not even be in memory for that matter, due to optimizations, for example). If you have such expectations, they are wrong. If you want to guarantee a particular layout, use arrays or #[repr(C)] structs or the like.

4 Likes

I don't think this is wrong, but I want to use it to understand Rust memory layout. I want to know the principle of 12-byte i64 offset, How the compiler is produced

A valid spec-compliant C implementation can store each variables into its own heap allocation and free it when it goes out of scope, to meet the expectation of the automatic storage duration. The Rust doesn't have formal memory model yet but I don't think it should be a lot different on this context.

Fun point is that the Go language actually stores each variables into its own heap allocation, but frees them when the GC decides so. The compiler optimization can elide the heap allocation if not necessary and store them directly on stack. This is called "escape analysis", a technique also implemented in some advanced JVM implementations.

Rust reserves the right to layout memory however it's optimization passes determine is best, according to the active optimization critieria. This includes the right to layout two instances of the same structure differently in different parts of the program. If you want to control layout order, use #[repr(C)] on a C-compatible structure. In all other cases any expectation you may have for relative layout of disjoint items is simply fantasy.

1 Like