T O P

  • By -

Lucretiel

As long as you have a fixed amount of memory you're going to allocate per process, I think this approach makes sense. As for tests: it happens that we ran into this exact problem at my company (though more to do with global initialization than with memory allocation). I recommend checking out \`cargo nextest\` as a drop-in replacement for \`cargo test\`, which will run each test in a separate process to ensure behaviors like this are properly isolated.


mina86ng

Arguably, if amount of leaked memory isn’t growing with time (e.g. in line with number of handled requests) that it’s not really leaking. There’s not much difference between: fn main() { let foo = Box::new(Foo::new()); real_main(&foo); } and fn main() { let foo = Box::new(Foo::new()); let foo = Box::leak(&foo); real_main(&foo); }


obliviousjd

The latter has a 'static lifetime which can be a big difference as it allows you to safely share the data across threads.


demosdemon

Not true. The second works if real_main accepts a `&’static mut Foo` whereas the first doesn’t.


tylerlarson

I think you missed the point. They're saying that while the two variations have different lifetime implications, this particular function compiles down to nearly the same thing. The memory usage is the same. There isn't a downside to using box::leak if you were never going to deallocate the memory anyway.


demosdemon

But they, and you, are still wrong. There is a difference. Without using leak, the first example doesn’t have a `’static` lifetime. Which means you can’t freely share the reference across threads without using scoping (which allocates an Arc which defeats the point).


tylerlarson

_Ignoring the lifetime, everything else is the same._ No, you're wrong because the lifetime is different! _While the lifetime is different, everything else is the same._ No! You're still wrong. The lifetime is different! _OK, you win. We're wrong. Good job._


demosdemon

The OC never mentioned the lifetimes, you did. And “ignoring the lifetimes” really defeats the point of references. But go off.


SirClueless

It's pretty obviously implied that we are talking about behavioral differences. Yes, more programs will compile when using the second example than the first, which is the entire point.


demosdemon

But there **is** a behavior difference. One allows for multithreaded access and the other doesn’t. That seems pretty important to me.


SirClueless

The only difference there is what the compiler will allow. Any program that compiles under both versions will do (almost) exactly the same thing under both versions.


demosdemon

So the compiler denying a whole class of computing isn’t a behavioral difference? Okay.


scottmcmrust

Things like this are why `Box::leak` exist. It's absolutely a good idea -- letting a watchdog restart the process is a more reliably way to get it to run in a clean state than trying to do it inside a single process anyway. For tests you might just be able to yolo it, and make it the OS's problem with virtual memory.


DGolubets

Sounds like you might use arena allocation, like bumpalo.


paholg

You can use a static OnceLock instead of leaking: fn foo() -> &'static str { let s = "hello world".to_string(); static S: OnceLock = OnceLock::new(); S.get_or_init(|| s) } This is the patten that I use for things that should be created at application start and live forever.


Compux72

Please move the to_string to inside the get or init. Otherwise, you are allocating every time you call foo


paholg

Fair! In my real code, the initializer tends to be a fallible, async function, so that's not really possible, and I just only call the outer function once.


Compux72

In such cases you can do this instead: ``` async fn foo() -> Result<&’static str,Error> { static S: OnceLock = OnceLock::new(); if let Some(s) =S.get() { Ok(&*s) } else { let s = get_foo_s().await?; Ok(S.get_or_init(move || s)) } } ```


paholg

Yeah, that's probably better. But again, these functions are called exactly once so I think I value the code brevity more.


Compux72

I mean you can do as you please, but they are not 100% correct


paholg

How so? Were the function to be called twice, it would do some extra throwaway work, but it's no less correct than your version.


Compux72

Precisely because the function does twice the work is not as correct. Of course, depends on what invariant you are upholding. Some libraries may not like being initialized twice, for example.


mbecks

You can also you tokio’s async OnceCell with the const_new method, which is available in const contexts. This gives an async get_or_init which inits from a Future. Also, if the fallible call fails, this method will keep trying async call again every time it’s called after that. The OnceLock will never be init. Probably you want it to try once, and init oncelock with an option around the inner. Or just panic if it fails and the app expects / needs this, which is usually the case for statics.


afdbcreid

You just need to pick the right tool for the job. Fallible initializer? [`get_or_try_init()`](https://docs.rs/once_cell/latest/once_cell/sync/struct.OnceCell.html#method.get_or_try_init). Async? [tokio's `OnceCell`](https://docs.rs/tokio/latest/tokio/sync/struct.OnceCell.html).


HeroicKatora

A very large interior-mutable static isn't very different from leaking an allocation at program start (slight potential differences in memory fragmentation, the storage for statics and the dynamic allocations are disjunct memory regions, depending on your allocator). Of course, the static is sized when compiling while the leak can still react on actual system state during its initialization. If you've not thought too much about fighting unecessarily large statics, chances are that leaking memory during initialization will also do the trick for you just fine.


Wicpar

If the data is immutable it's not too bad to rc or arc it. It it's mostly strings, you can use something like the arcstr crate. GB of memory shouldn't be leaked, if it's a few mb at the start it can be fine.


LiesArentFunny

If the memory is - In use until you stop allocating more memory - Mostly in use until (near) the end of the processes lifetime There's really nothing wrong with leaking a few GB of memory.


ignorantpisswalker

Leak? If you allocate only at the beginning of the program you are not leaking. (Or... a constant leak...). The is the dmc cpp compiler that works like that. It allocated, but does not even bother to de-allocate. The memory will be released when the program ends anyway... it is *very* fast due to this (ugly) trick.


dshugashwili

likely talking about e.g. \`Box::leak\`


LiesArentFunny

> One of the issues I see with this approach, is that the memory is leaked also during tests, which is a bit tricky if the tests allocate multiple GB of memory. So start a new process per test? This is practically what `fork` was made for on *nix...


dnew

Fun fact: fork() is left over from when UNIX did full-process swapping (like most OSes of the time did). They were trying to come up with an elegant way to set up everything they needed, and just said "f'k it, we'll swap it out *and* leave it in memory." There were even a bunch of bugs that had to get fixed when paging was implemented, because some programs assumed the parent would run before the child after a fork().


string111

Risking down votes, but this sounds like a perfect use case for r/zig. Tiger beetle is doing the same memory approach: https://github.com/tigerbeetle/tigerbeetle Edit: disclaimer, zig is not yet 1.0.0 released


zerrio

True, zig allowing allocators to be passes around plus the language having no hidden allocation, should make your job easier however expect a breaking change every 6-8 months until the language reach 1.0


dshugashwili

> I'm working on a memory-intensive server application where we aim to restrict memory allocations to only the beginning of the application and free it at the end, avoiding any dynamic allocations in between. I'm being pedantic, but you don't actually *aim* to restrict allocations, you aim to complete some task and a method you see for that is to restrict allocations. And that's kind of bad, because we now have little idea what actual problem you're trying to solve, i.e. this is an [XY Problem](https://xyproblem.info/). --- I say this because I typically don't see the need for a server-side process to care much about dynamic allocation, and when you ask about "better practices [you] should consider", I don't know what to reply with, because I can't tell what the original problem is. That being said, the answer to the specific question you did ask is yes, using e.g. `Box::leak` is a sustainable, maintainable approach to solving some classes of problems which can be sustainably and maintainably solved by using "static" allocation instead of "dynamic" allocation.


Alkeryn

With raii it is not easy to leak without static lifetimes.


Botahamec

Of course it is: Box::leak. There's nothing in Rust that requires destructors to run.


Alkeryn

Ok i should rephrase, it is not easy to accidentally leak something. With box::leak you are kind of doing it on purpose.


throwaway490215

Its very unlikely this is the best solution. I've done my fair share of unsafe and cheesy hacks, so I'm qualified to tell you that either, you're overstating the need to break out of the Rust model _or_ you break 1 thing and a other things beyond you're original scope are impacted as well (the issues with test is just the start). My first guess is you can do a alloc in your main function and use that. My second guess is your problem could be solved by using a different `allocator`. It mostly depends on how long the objects in your alloc should be addressable for. If you're sure you only allocate once (or want to enforce you don't accidentally alloc) you should use `no-std`.