1

I've been working with Rust in a constrained embedded environment (on the STM32F303 MCU), and I noticed that some of my functions were allocating an unexpectedly large amount of stack space. In this environment, I do not have an allocator, and need to allocate large, mutable, static data structures on the stack. Eventually, I found that some functions were unexpectedly allocating space on the stack, and causing my memory-constrained stack to overflow.

I am looking to understand how much memory needs to be allocated on the stack for the following mutate function. I've been searching for answers to this problem on this site, but none seem to give an answer for this more general problem. I understand that for this code block below I can look at the LLVM/assembly to see where the allocations are made, but I'm trying to understand how to predict when stack allocations are made for a general class of problems- independent of compiler options and the optimizer. The following problem is a toy example which mimics the pattern I'm using (and having issues with) in my embedded Rust program.

Question: How much memory does mutate below need to allocate on the stack?

struct Parent {
    data: Option<[u32; 1024]>,
}

impl Parent {
    pub fn new() -> Self {
        Parent { data: None }
    }

    // Ideally, this function should allocate negligible memory on the stack
    pub fn mutate(&mut self) {
        let arr = [0u32; 1024];
        self.data = Some(arr);
        for i in 0..self.data.unwrap().len() {
            self.data.unwrap()[i] = i as u32;
        }
    }
}

fn main() {
    let mut it = Parent::new();
    it.mutate();
}

Considerations

  • Other posts seem to suggest this behavior is up to the optimizer. If this is the case, is there a way I can rewrite the code above so that each function's stack allocation size is obvious to the reader of the code?
  • Does the let keyword (first line in the mutate function) have any affect on whether the large array is allocated on the stack?
  • I am sure I could dip into unsafe rust to ensure this function doesn't need to make any allocations/memcpys (like I could in C). Is there a better way to do this without using unsafe?

Thanks in advance for any help on this issue.

  • if you think Option save you from using the array size you wrong. I don't understand what you expect... This look like a variance of xy problem. – Stargateur Apr 14 '21 at 18:07
  • what's wrong with https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e2d2ab6a0507d3b93daf6d24e5519efc ? – Stargateur Apr 14 '21 at 18:16
  • 2
    I hope you realize that whole `for` loop does nothing because each `unwrap()` makes a copy of the whole array. – trent Apr 14 '21 at 18:28
  • Thank you for these responses. I'm aware that the Option here will allocate more than the size of the array, and that it cannot be used to save space. In my program, the Parent struct may be in a number of states, one of which where the array is not ready to be used, and another when the array has been intialized by a member function to Some, and is ready to be used in calculations. – Kevin Kellar Apr 14 '21 at 18:44
  • @trentcl, can you show a small snippet which allows me to mutate the elements of the array without needing to copy the array? – Kevin Kellar Apr 14 '21 at 18:45
  • 1
    Mutating the elements of the array doesn't require copying anything (except the individual values as you mutate them). You're copying the array *instead* of mutating it. You basically have [this problem](/q/32338659/3650362) except `[u32; 1024]` is `Copy` so there's no ownership problem. If you mutate the elements of the array without using `unwrap`, [it's *initialization* where the extraneous copy happens](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=33e5c55ca31b6545faa601d5264965cd), which is hard to get rid of, but should be fairly tractable to LLVM. – trent Apr 14 '21 at 19:01
  • @trentcl Thank you for that example. Like you note- initialization makes an extraneous copy, and in my embedded debug environment, that allocation isn't being optimized out. – Kevin Kellar Apr 14 '21 at 19:14
  • @trentcl As a result, I'm looking for a pattern to follow which will never need to allocate the data on the stack first- rather mutate the memory which has already been allocated for the data in the Parent struct's Option field. – Kevin Kellar Apr 14 '21 at 19:15

1 Answers1

1

Question: How much memory does mutate below need to allocate on the stack?

4kB (32 = 8 * 1024)? You're literally creating an array local on the stack, and Rust doesn't have placement new so even if you did not create an array local on the stack it's basically up to the optimiser where it handles the allocation.

Also note that your Parent struct necessarily always takes 4kB as well, in fact it likely takes 4kB + 4 or 8 bytes (not sure what the alignment requirements are for an array of u32) for the Option's tag as the array has no invalid value which rustc could use for niche variant optimisation.

edit: oh wait no, you'er also copying the array back into the function with the self.data.unwrap() calls, so that's 2 more of those, so at least 12kB. In fact plugging this into Compiler Explorer it tells me:

example::Parent::mutate: mov eax, 28856

so that's 28k, not quite sure where the extras come from.

Anyway activating -O, it looks like it all gets optimised away so… lucky?:

example::Parent::mutate:
        push    rax
        mov     dword ptr [rdi], 1
        add     rdi, 4
        mov     edx, 4096
        xor     esi, esi
        call    qword ptr [rip + memset@GOTPCREL]
        pop     rax
        ret
Masklinn
  • 34,759
  • 3
  • 38
  • 57
  • Thank you for your response. I'm looking to rewrite the mutate function so that it will not need to allocate memory on the stack- rather use the memory already allocated in the parent struct. Is there a way to write this function so that its memory footprint is negligible- independent of the optimizer? – Kevin Kellar Apr 14 '21 at 17:44
  • @KevinKellar removing the `Option` from the structure (without an indirection it doesn't actually save any memory) then working in-place on the member? If you insist on working with an `Option` member, then using something like `if let Some(a) = self.data.as_mut() { iterate and manipulate array here }` would help a lot to get a reference (a pointer) to the array inside the option if there is one, rather than move data in and out. – Masklinn Apr 14 '21 at 17:50
  • I'm not using the Option to save memory. In my program, the Parent structure needs to be able to be constructed without initializing the data inside the Option- thus it is set to None in the constructor. – Kevin Kellar Apr 14 '21 at 18:48
  • If the parent struct's field was an Option, how could I initialize self.data to Some without having to allocate space for an entire Otherstruct on mutate's stackframe? In C, a function could take a pointer to the unitiaized memory within the Option, and mutate the memory using the pointer. But in rust, can I make a reference to an Option's Some variant, but leaving the data within the Some uninitialized? – Kevin Kellar Apr 14 '21 at 18:52
  • I am looking for a way to rewrite the mutate function to allocate no memory independently of compiler/optimizer settings. – Kevin Kellar Apr 14 '21 at 18:54