How to change type of variable without stack memory consuming

#![allow(warnings)]

#[repr(C)]
struct Foo0 {
    bar: u64,
}

#[repr(C)]
struct Foo1 {
    bar: u64,
}

use std::mem::transmute;

fn main() {
    let mut foo_0 = Foo0 { bar: 1 };
    println!("Foo0:         {:p}", &foo_0);
    let foo_0 = unsafe { transmute::<_, Foo1>(foo_0) };
    println!("Foo0 -> Foo1: {:p}", &foo_0);
    let next_var = 0u64;
    println!("next_var:     {:p}", &next_var);
}

Output cargo r --release -q:

Foo0:         0x7fc8636f90
Foo0 -> Foo1: 0x7fc8636f98
next_var:     0x7fc8636fa0

next_var is not placed where the first foo_0 was, so that memory space is permanently wasted for no good reason (because that memory is not occupied by anything, why not reuse it?).

See this conversation and its citations.

2 Likes

The previous variable is still there, though. Perhaps you are confusing shadowing with mutating in-place.

For this one in particular, if you just stopped looking at the address, none of them would take any stack space, since Avoid `alloca`s in codegen for simple `mir::Aggregate` statements by scottmcm · Pull Request #123886 · rust-lang/rust · GitHub keeps it all in registers instead.

6 Likes

TLDR stop trying to analyze the compiler from within. We have apps for that.

Compiler explorer is your friend.

3 Likes

It's so complicated, I didn't understand much of it. As I understand it, stack is either guaranteed not to be wasted, or it depends on unreliable LLVM optimizations, and reliability can only be obtained by rewriting the program in asm. By the way, if the second option is true, it would be interesting to know if the same problem exists in zig, because I started looking in its direction because of such unrealizable things in rust

Why couldn't you just make a similar instruction:

\\ something like compiler intrinsic
reinterpret!(ToType1, from_instance_of_type_0);

And that after this instruction the variable with the same address should change its type for the compiler. It seems that with such an instruction there would be no need to rely on LLVM optimizations. Perhaps this solution would spoil other optimizations and the problem is not in rust but in LLVM, because it cannot change the type of a variable located in the same address?

Compiler explorer gives me asm, but I don't understand much about asm, it's too complicated, and I don't think I need to know it, I think it can be explained in a simpler way. And moreover, for example, if everything depends on unreliable optimizations, then asm will not give me a reliable answer to the question, I would like an answer from engineers who understand whether what I want will happen 100% or not.

If you want to understand how it works on the machine level, learning the basics of assembly is the only reasonable answer.

3 Likes

Very few optimizations are guaranteed ("will happen 100%").

If you have a semantic necessity to reuse the exact same storage for some reason, you need to be using some mechanism that respects those needs. For example, take a &mut _ to the variable, do a conversion dance (&mut Foo0 to *mut Foo0 to *mut Foo1), and then write to the memory that way. Or use a union maybe.

If you don't have a semantic necessity to reuse the same storage and you don't have some particular reason to care about how optimized your stack frame sizes are, don't worry about it. This particular optimization falls into "almost no-one needs to actually care" IMO.[1] Some other considerations, be they logical or performance based, will matter much more in practice.

If you do have some particular reason to care... you should learn enough ASM to understand what's going on, or maybe present the specific reason and ask for advice on that.


  1. There are cases where you have to care about stack space, but the OP isn't one of them. ↩︎

3 Likes

Note that you don't have to just learn ASM, there's a variety of tools to let you know what's changed, for example you could check if using the suggestions of casting a pointer or using a union makes any difference.

You might want to try two different ways to write the same function, and see if the output is substantially different (e.g. other than names), or you could look at the Rust MIR and llvm IR to see how Rust transformed your code into LLVM's view of instructions, then the LLVM optimization pipeline to see how it decided to transform your code into assembly.

Just because lea is a meaningless term or xor eax, eax is bizzare to see, doesn't mean Compiler Explorer can't help you understand what's happening at every level.

But yes, if you intend to care about the specifics at this level, you should probably learn at least enough of assembly to be able to read it. It's mostly pretty straightforward:

  • mov foo, bar is foo = bar;
  • add foo, bar is foo += bar;, etc. (so xor eax, eax is eax ^= eax;, e.g. eax = 0, but smaller due to x86 being weird)
  • call foo is foo(...), where the arguments are what's in registers and on the stack (this is what a "calling convention means"
  • ret is return

Thats most of the instructions you need to care about, so other than picking up what registers do what and knowing about the stack and so on, there's really not that much to it.

In particular, your example without printing the pointers and using an extern function to avoid it just completely eliminating the entire body[1]:


extern { fn bar(foo: Foo1); }

pub fn foo() {
    let foo_0 = Foo0 { bar: 1 };
    let foo_0 = unsafe { transmute::<_, Foo1>(foo_0) };
    unsafe { bar(foo_0) };
}

optimizes (don't forget to pass -O to the compiler) to this LLVM IR:

define void @example::foo::hb73cef95ff019d63() unnamed_addr {
start:
  tail call void @bar(i64 1)
  ret void
}

declare void @bar(i64) unnamed_addr #0

Where you can see it has completely optimized away literally everything. The assembly is just:

example::foo::hb73cef95ff019d63:
        mov     edi, 1
        jmp     qword ptr [rip + bar@GOTPCREL]

(apologies to those who hate intel syntax)

That is just:

  • set a register to 1 (this being the register used for the first argument to a call)
  • "jump" to bar - this is an optimized call since it's the last thing we are doing in this function so returning from bar is also returning from foo.

You can play around and look at references for assembly and pick the important stuff up really quick, if already are caring about stack layout.


  1. I didn't use std::hint::black_box here because it generates more LLVM IR and a confusing lea instruction I didn't want to get into. ↩︎

2 Likes

No, that's false. Nothing like that is guaranteed.

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.