Control the timing of dynamic allocation

Hello everyone,

I have a specific question about allocation. I would like to control the timing of dynamic allocation. #help

Context

My company needs a very advanced knowledge of memory usage for embedded devices.
We use C++ and dynamic allocation is only allowed at the beginning of the program (init phase), but not afterwards.

To do this, each object must implement 2 functions: allocate() / unallocate() in the allocation/init phase.

My question is how to manage the moment when allocation is done (as with malloc/new in C/C++).

We use pointers a lot in C/C++.
The equivalent in Rust is Box (+ Rc and Arc).

Question

  1. I want to allocate memory on the heap without initialising it.

To do this, there is Box.new_uninit(). However, this is an experimental function.

  1. Is there another way via the stable toolchain? It's not mandatory, but it's better.

Example (C++)

Let's take a simple example of a buffer object (such as an image class):

class ImageBuffer {
    ImageBuffer (int bufferSize) : m_bufferSize(bufferSize) {} // constructor
    ~ImageBuffer () { unallocate() ; } // destructor
    void allocate() { m_ptr_data = new float[this->m_bufferSize] ; }
    void deallocate() { delete[] m_ptr_data; }
    // and other methods...
    void do_something() {...};

    float* m_ptr_data ;
    int m_bufferSize;
}

int main() {
    // Init phase
    ImageBuffer img = ImageBuffer(10*10);

    // Allocation phase
    img.allocate();

    // Calculation phase
    img.do_something();
    // ...or send img to other function ...

    // Deallocation phase
    img.deallocate();

    return 0;
}

In this example, the buffer (m_ptr_data) is allocated exactly when I want it and is initialised at another time.

Conclusion / Need Help

I'm looking to do something like this in Rust. The allocation phase is mandatory. And it is strictly forbidden to do dynamic allocation outside the allocate() function, as vectors (std::vector or std::vec) do. So I'm looking for a way of controlling the allocation of all the data (on the heap).

Maybe my reasoning is wrong and I should change my method, but in that case would you have an idea for controlling dynamic allocation and being able to use container/buffer/vector/... which don't do implicit dynamic allocation?

Just compose it from the already existing parts:

let box_uninit = Box::new(MaybeUninit::uninit());
2 Likes

There are various safe abstractions over uninitialized memory with various APIs that you can use instead of working with MaybeUninit directly. Here are a few useful ones off the top of my head, but there are lots of others as well:

  • Vec::with_capacity() will allocate immediately, but it will try to implicitly allocate later if you exceed the capacity you started with.
  • arrayvec defines fixed-capacity vector and string types that you can place into a Box if you want them to exist on the heap, or you can store them inline within a struct that is itself stored on the heap somehow.
  • You can put an Option::None or a OnceCell into a box to place it on the heap, and then write into it later.

This sounds like a situation where you might want to override the global allocator with one that can be switched into a mode that will either panic on alloc() or otherwise log a rules violation once the initialization phase is over.

2 Likes

Yes indeed, this could be a solution for uninitialised data.

It is not possible to use Vecs as is, but perhaps modifying the Vec allocator could be a solution. "Vec<i32, CustomAllocator>"
I would imagine that any dynamically sized structure would implicitly implement an allocation trait.

arrayvec doesn't implement an allocate() function, it's just an array disguised as a Vec and therefore requires a known size at compile time.

std::alloc::GlobalAlloc is an unsafe trait that is not desirable.

I'm looking for a container that doesn't allocate memory implicitly but can dynamically allocate memory only when asked (an allocate() function).
A Vec with explicit allocation would be perfect: no implicit allocation and therefore an allocate() function to keep the dynamic/explicit side.

I find it hard to see how defining my own allocator can be useful here. It probably is, but I don't see how.

It won't help with solving your problem directly, as you still need to find solutions that allocate when you expect them to. What it can do is enforce your allocation requirements on any type that implicitly allocates, which means that you can leverage types like Vec knowing that accidentally allocating late will fail.

Something like this (untested, just a sketch):

use std::alloc::{GlobalAlloc, System, Layout, handle_alloc_error};
use std::sync::atomic::{AtomicBool, Ordering::Relaxed};

pub struct MyAllocator {
    initializing: AtomicBool,
}

unsafe impl GlobalAlloc for MyAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if self.initializing.get(Relaxed) {
            System.alloc(layout)
        } else {
            handle_alloc_error(layout)
        }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        System.dealloc(ptr, layout)
    }
}

impl MyAllocator {
    pub fn end_initializtion(&self) {
        self.initializing.set(false, Relaxed);
    }
}

#[global_allocator]
pub static ALLOC: MyAllocator = MyAllocator { initializing: AtomicBool::new(true) };

(based on example here)

You can wrap a Vec in a newtype and only expose non-allocating APIs:

struct NoReallocVec<T>(Vec<T>);

impl<T> NoReallocVec<T> {
    fn new(capacity: usize) -> Self {
        NoReallocVec(Vec::with_capacity(capacity))
    }

    // only implement functions that don't push or extend
}
1 Like

My company needs a very advanced knowledge of memory usage for embedded devices.
We use C++ and dynamic allocation is only allowed at the beginning of the program (init phase), but not afterwards.

Is this only to avoid OOMs due to constrained memory or for other reasons?

I'm looking for a container that doesn't allocate memory implicitly but can dynamically allocate memory only when asked (an allocate() function).

In case it's only about OOM avoidance, have you considered using fallible containers instead which will try to allocate but return Result (which is #[must_use]) indicating success/failure. Which means any allocating operation would be quite visible, even if it's not strictly a separate method.

std::alloc::GlobalAlloc is an unsafe trait that is not desirable.

If you want to track all allocations, including those that might happen under the hood (if you're using alloc or std) then you'll want to override that anyway. But if your requirements are strict you might have to stick to core anyway and use [no_std] crates that support your requirements.

A Vec with explicit allocation would be perfect: no implicit allocation and therefore an allocate() function to keep the dynamic/explicit side.

A few methods on Vec allow it to be used like that, but that's only a tiny fraction of its API surface. Namely {try_}reserve, push_within_capacity, split_at_spare_mut/spare_capacity_mut + set_len

You could piggyback on the linux kernel work that added #[cfg(not(no_global_oom_handling))] to alloc which will disable most methods that allocate. It's not a stable feature, so you'll have to do a custom build of the standard library to use that

Thanks for your reply

In fact it's sometimes to avoid OOMs (but that's not really a problem in my case) but it's mainly to be very robust, and to avoid nasty surprises from the OS. And other reasons I don't know the details of.

Fallible_collections are a good idea and are almost ideal. The best thing in my case would probably be to overload the containers in the standard library, as H2CO3 suggests. It still requires some work.

Vec's methods implicitly allocate data when some is missing.
Using with_capacity and push_within_capacity meets my needs. All I need to do is forbid the creation of vec outside an explicit allocation method.

Something like that:

struct Img {
    data: Vec<f32>,
    width: usize,
    height: usize,
}

impl Img {
    fn new(width: usize, height: usize) -> Img {
        Img { Vec::new() , width, height }
    }

    fn allocate(&mut self) {
        self.resize_with(self.width*self.height, Default::default);
    }

    fn deallocate() {} // deallocate is useless in Rust no ?

    fn set_pixel(x: usize, y: usize, val: f32) {
        self.data[x + y * self.width] = val;
    }
}

fn main() {
    // Variable declaration phase
    let mut img = Img::new(1920, 1080);

    // Allocation phase
    img.allocate();

    // Calculation phase
    img.set_pixel(...);

    // Automatic deallocation phase
}

Is it possible to dynamically disable dynamic allocation in Rust? That would be amazing for preventing accidents.
There's an experimental Vec::allocator() method that returns an allocator reference but if I had a mutable allocator reference I could switch between the default allocator and an allocator that always returns an error.

The two options are replacing the global allocator or the unstable A: Allocator generic on various collections with one that supports toggling. You'd still have to deal with the errors though when something tries to allocate while the allocator is disabled.

fn deallocate() {} // deallocate is useless in Rust no ?

You can still release the memory held by a struct if that's desirable. Just like shrinking a Vec.

I'm not sure how to proceed now, but I think it's the best solution for the moment.
Do allocator interfaces have to be unsafe?

like this:

fn deallocate() {
    vec.clear();
    vec.shrink_to_fit();
}

Thank you for your help.

In the general case they are unsafe whether you slap that label on them or not. If you pass random pointers to deallocate until you hit something that exists you're going to cause unsoundness. Maybe one could design a foolproof API using scopes, branded lifetimes, authenticated pointers or whatever. But those would be too restrictive for many allocator uses.

I want to allocate memory on the heap without initialising it.

I wonder if the bumpalo crate would work for you. You can preallocate memory at the beginning of the program and then "allocate" from that memory. Or if there's another slab/arena crate that would fit your use case better

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.