I will be using the glam crate as an example for my question but I am trying to understand stack frame allocation in the general sense. Looking at DMat4 from glam crate, there are methods such Add and Mul that take self and Self return Self. DMat4 implements Copy. I can do a series of computations like:
fn bar() -> DMat4 {
let mat_a = DMat4::default();
let mat_b = mat_a.add(other_mat);
let mat_c = mat_b.add(another_mat);
… one-hundred-similar-lines
let mat_d = mat_c.add(yet_another_mat);
mat_d.add(final_mat)
}
Each time I Add, a stack frame is pushed with allocation for 256 bytes (128 for each matrix) and then the frame is popped. Not too bad? However, imagine that with a hundred intermediate matrices. Then the stack frame for bar itself would be 12800 bytes!
The Add method could take &mut self and &Self. In that case, each Add stack frame would only allocate enough for two pointers and the bar frame would only need 128 bytes for mat:
fn bar() -> DMat4 {
let mut mat = DMat4::default();
mat.add(other_mat);
mat.add(another_mat);
mat.add(yet_another_mat);
… one-hundred-similar-lines
mat.add(final_mat);
mat
}
Is that better in terms of speed and memory usage or does it work out to not usually matter on most hardware? Maybe the compiler is smart enough to not actually allocate 12800 bytes for the first snippet? And if so, should I not rely on such hopeful optimizations?
The stack frame for bar is only ever plausibly that large if optimizations are completely disabled. With optimizations enabled, space used by variables no longer in use will be reused — and that’s assuming that inlining doesn’t lead to optimizing away all moves entirely.
There are situations where explicitly starting from in-place mutation (by using &mut self) gives performance benefits. Matrix math is, as far as I know, not one of them, because what you actually want is for everything to get inlined so the optimizer has access to all the additions and multiplications, and the intermediate matrix values no longer need to exist at all.
You can reuse the same variable regardless of whether the add method takes self by value or by reference:
let mut mat = DMat4::default();
mat = mat.add(other_mat);
mat = mat.add(another_mat);
But this is unlikely to affect stack use: the compiler converts the code to the static single assignment form which is the same either way.
The compiler will only use a lot of stack if it needs to track many values at the same time. Here, you don't need the old values, the compiler knows it, so it will reuse the stack space.
Also, remember that passing "by move" will actually pass a pointer anyway for larger types. If you're passing a 16× f32 type, what's emitted to LLVM will pass a pointer whether you write self or &self.
Except in this case there will also be a copy (if the type is Copy then always (unless LLVM can optimize it), if not, I think today still but maybe after MIR move elimination not).