This reminds me of the exploding ultimate GC technique [1]:
> on-board software for a missile...chief software engineer said "Of course it leaks". ... They added this much additional memory to the hardware to "support" the leaks. Since the missile will explode when it hits its target or at the end of its flight, the ultimate in garbage collection is performed without programmer intervention.
[1]: https://devblogs.microsoft.com/oldnewthing/20180228-00/?p=98...
[0]: https://blogs.oracle.com/javamagazine/epsilon-the-jdks-do-no...
And then someone tries to use your compiler as a service (code analysis, change triggered compiler) and it's a dead end
We wrote the entire thing (and tested, using ASAN and fuzzers and other techniques) to avoid leaking memory, and then strategically inserted [the equivalent of a rust `mem::forget`](https://github.com/sorbet/sorbet/blob/0aae56e73c7680ec6053b3...) into the end of the `main` driver during standalone mode, to avoid calling those destructors when we're about to exit anyways.
This optimization is definitely still relevant for new systems today.
We don't go around saying "oh, you didn't want modulo 5 arithmetic? You should've put that in the spec, not rely on some contrived absolute truth".
C++ had its very first language servers via Lucid's Energize C++ and Visual Age for C++ v4.
Here is the 1993 video and related paper from Lucid.
https://www.youtube.com/watch?v=pQQTScuApWk
http://www.dreamsongs.com/Cadillac.html
And some information regarding VA, unfortunately most is missing from online world.
https://www.ecomstation.it/pido2/home/esterni/vac40os2.pdf (codestore)
http://www.edm2.com/index.php/VisualAge_C%2B%2B_4.0_Review
It is also why Delphi, VB and C++ Builder were already such a pleasure to use versus the Makefile and vi world of UNIX.
I see not freeing as a clever bug/workaround with nice positive side effects. Not as a clever solution.
The user doesn't care that each allocation is paired with another call to free it up. They just care if the program runs quickly and doesn't use too much memory overall.
> They just care ...
It all depends on who "they" are and what they want to achieve.
Yes they do. In most programming languages, by default integer arithmetic is modulo 2^64 (at best). If you want arbitrary precision arithmetic, you have to explicitly specify that.
If this is incorrect, then every modern malloc implementation is incorrect.
I understand it's tradeoffs and we all have real-world limitations to contend with -- but again, of all the corners that could be cut that's exactly the one I didn't imagine they would.
Nasty.
One of them was unburdened by any thought of freeing stuff, and relied entirely on the application exiting for cleanup. This was very convenient to work with, and never ended up posing an issue.
Another used a series of allocation arenas, where certain arenas would be cleared at certain points in the compiler pipeline. This made for both speedy alloc/freeing and avoided leaks, since you weren't at risk of "forgetting" a data structure. It was also a major headache to keep track of exactly what the longest lifetime of a long-lived datastructure might be, and to pick an arena that won't be cleared in the meantime. Unfortunately the programs compiled with this compiler were large enough that we certainly couldn't have gotten away with just leaking memory; we sometimes OOMed as-is!
The third used standard C++ memory management. This compiler was quite simple, and the vast majority of its data used stack-based lifetimes. For a more complex compiler this would've become a headache.
I think that all of these compilers chose the correct allocation strategy for what they were doing. "Good practices" aren't as universal as we might like to believe, they depend entirely on the context in which a tool is designed to operate. And yes, we can guard to some extent against that context changing, but for the most part that's why we keep getting paid.
I was taught -- including at the start of my career when I used exclusively C/C++ (about 18.5y ago) -- to take care of all resources I was using and not rely on runtimes.
I understand and appreciate different usages but to me doing a proper cleanup was the sane default for most programmers. And that's all what I was saying.
Obviously, as one digs deeper in a specialised area where more and more efficiency is demanded then they have to reach for tools that most of us wouldn't normally. That's quite normal and was always interesting for me to read about.
I don't think it's even that uncommon. I believe some HFT firms run Java with a huge amount of RAM and GC disabled, and get around it by just rebooting the software occasionally.
To me writing software like that is fair game, I don't see the point in being dogmatic about "how things should be done".
I recall reading somewhere, years ago, that some OSes couldn't be relied upon to release unfreed memory when a process terminated. In those contexts, fastidious freeing would be important even in short-lived processes.
In my own C code I tend to free everything so that I get a clean trace from Valgrind and don't risk masking legitimate bugs, but I typically write long-running daemons.
For specialised apps and servers it's of course a perfectly good practice.
But still, in a world where languages and runtimes are also judged by their ability to run in lambda/serverless setups, I'd think this practice will start being obsolete, wouldn't you think?
(What I mean is that I imagine that any serverless function that runs in severely constrained and measured environments like the AWS Lambda would gain a significant edge over the competition if it did an eager cleanup. Should allow more of them to work in parallel?)
The leak to watch out for is reduced to the persisted data (eg in S3) between executions
I suppose, judging by the downvotes, that some find that perspective naive so I'll just cut it here because no productive discussion is happening. :(
You only need to care when you're at risk of OOM'ing during runtime. If you're generating too much garbage, you either need to start cleaning up after yourself, or you need to generate less of it -- but it doesn't matter which choice you take from the perspective of the OS or the server-less function.
The only reason I can think of for server-less to impact the validity of this pattern is from it's lower memory capacity -- but otherwise it's the same constraints. And anyways, it's probably not nice to generate a few GB of garbage even if the user has the space
Because a language server needs to do a lot of the same work as a compiler.
This is an eventual hope for rustc. For now, the latest language server and it share a bunch of libraries, but language servers are effectively compiler frontends.
And how many millions of iterations have been done successfully in that "awful" system?
The very fact that you never imagined it I think says a lot.
As I acknowledged in other comments of mine downthread, I understand that different situations require different tradeoffs. It's just that forgoing memory deallocation wasn't one of them in my head.
If I allocate just enough memory, but not too much, then pauses for defragmentation of free space may be costed to the code that calls me.
A solution to this that I’ve seen in soft real time systems is to amortize cleanups across all allocations. Every allocation performs n steps of a cleanup process prior to receiving a block of memory. In which case most of the bad actors have to pay part of the cost of memory overhead.
Might be good for Rust to try something in that general realm, or in the cleanup side may be easier to tack on. On free, set a ceiling for operations and queue what is left. That would at least peak shave.
You can tell that it's unrelated cleanup because if it were related, then the cost of freeing wouldn't be noteworthy. It would be cache hot because you would be visiting it for the second time. In which case we'd be talking about why you are scanning a giant object on an event loop in the first place. That's not what's at issue. What's at issue is that you've been handed this great bomb of uncached data from someone else and now you're stuck doing the janitorial work.
Freeing an object of arbitrary size is effectively an unbounded operation. Cache invalidation has a very high cost, sure, but it's still bounded.
Putting a limit on the amount of work you do, you could stop before purging the entire cache. You could use a smaller limit on doing work for someone else and control invalidation there, too.
In theory, you could also use a memory pool in Rust but I think the standard library uses malloc without some way of overriding this behaviour.
Still, bumpalo has been used to great effect in dodrio[1], a React-like library for Rust with really good performance.
It’s more awkward, but I much prefer Zig’s approach here where everything that allocates takes an allocator as a parameter. Usually the allocator is specified at compile time - in which case zig can generate identical code to the rust compiler. But when you want the flexibility, it’s there.
Aside from compilers, this is heavily used in video games where there’s often a lot of objects that get allocated per frame, and can be discarded all together. And in that case rust’s lifetime tracking would be a huge asset. The dovecot email server (C) also makes superb use of a mix of memory containers for different tasks. Given how messy email parsing is, dovecot is an absolute pleasure to read.
This is also the C++ approach
As far as I know it would require both Rust's borrowing semantics and Zig's architectural choice.
From purely my own fan-boy perspective Zig approach is something that I would have really liked for Rust to adopt (I have no idea of which one came first chronologically).
This is routinely done in medium to large C++ programs for different reasons (performance, debuggability...).
Edit: this will also break any code that relies on Drop being called for clean up, but that is already a "suspect"/incorrect pattern because there are no assurances that it will ever run.
Yes and no. Whenever control leaves a code block, Rust automatically calls the drop() method of all values still owned by that block. There is no guarantee that control will exit every block (cf. Turing), but a moderately exceptional circumstance needs to occur for this not to happen, like an infinite loop.
@autoreleasePool { [[[SomeObject alloc] init] autorelease]; }//someobject is release after exiting this scope
//inside main runloop, not inside @autorelease{} [[[AnotherObject alloc] init] autorelease];
//AnotherObject will be release at end of main runloop
[[NotAutoreleased alloc] init]; //will leak past runloop iteration
is that understanding correct?
TR::Region is the slab allocator used by the JIT in OpenJ9/OMR. The linked commit adds functionality for calling destructors of arbitrary types allocated in the Region.
#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;I was thinking of the following code, where I believe the assignment to y is actually free. Though apparently this isn't called a "move".
let x = <<large owned type like [char; 1000]>>;
let y = x;
More info: https://doc.rust-lang.org/rust-by-example/scope/move.htmlIn the example from the article it is probably actually a copy because the value was originally on the parent thread's stack, which will be reused after the function returns, so the value will need to be copied to the new thread's stack.
However it is important to not that it isn't a deep/recursive bitwise copy. It just needs to copy the HashMap itself (which is probably a handful of words).
So yes, it is doing a bitwise copy, but this is also very cheap. It will be much, much cheaper than spawning the thread.
std::map<...> foo;
someFunction(std::move(foo));
And not moving a pointer like: std::unique_ptr<std::map<...>> foo = ...;
someFunction(std::move(foo));
So it copies sizeof(std::map<...>), not a pointer.I.e. a `HashMap` struct, or `Vec` struct don't directly contain the data.
For example the `Vec` is defined internally as something similar to:
`struct Vec<T> { data: *mut [T], capacity: usize, len: usize, marker: PhantomData<T> }`
(Slightly simplified, not actual Vec type).
So a move of a Vec copies at most 3 usize (24 bytes on 64bit systems), similar thinks apply for a HashMap.
Additionally the copy can often be elided through compiler optimizations.
As a interesting side note a new empty Vec/HashMap will not actually allocate any memory, only once elements get added it will start doing so. This is why it crates vec's of vecs of length 1. Or else it wouldn't need to do "number of element" free calls.
However, these copies can often be elided by optimizations.
In Clang, this flag is `-Xclang -disable-free`. Not from a Jedi...
For example, I had the "million strings" problem once, literally millions. The solution was to put every string into a single large buffer and the pointers in another buffer. Not only I could deallocate everything at once but I also saved a bit of RAM by not aligning (not needed for strings).
Only if badly designed. That is why it is contrived!
> While that may seem contrived, consider a vector of 1 million strings, something that's not too uncommon
A program dealing with a million elements of any kind should not be performing naive allocations to begin with.
> we do deallocation trickery in the real world
Skipping deallocations is an optimization, not a design pattern.
In other words, the code needs to keep the ability to perform the deallocation for debugging, testing, usage as a library, etc.
* The example code is obviously contrived. The real gist is that massive deallocations in the UI thread cause lag, which the example code proves. That very thing can easily happen in the real world.
* I didn't see any difference on my machine between a debug build and a release build.
* The example is preforming 1 _million_ deallocations. That's why it's so pathological. It's not just a "large" vector. It's a vector of 1 million vectors. While that may seem contrived, consider a vector of 1 million strings, something that's not too uncommon, and which would likely suffer the same performance penalty.
* Rust is not copying anything, nor duplicating the structures here. In the example code the structures would be moved, not copied, which costs nothing. The deallocation is taking up 99% of the time.
* As an aside, compilers have used the trick of not free-ing data structures before, because it provides a significant performance boost. Instead of calling free on all those billions of tiny data structures a compiler would generate during its lifetime, they just let them leak. Since a compiler is short lived its not a problem, they get a free lunch (pun unintended), and the OS takes care of cleaning up after all is said and done. My point is that this post isn't theoretical, we do deallocation trickery in the real world.