Presumably this is referring to Rust, which has a borrow checker and slow compile times. The author is, I assume, under the common misconception that these facts are closely related. They're not; I think the borrow checker runs in linear time though I can't find confirmation of this, and in any event profiling reveals that it only accounts for a small fraction of compile times. Rust compile times are slow because the language has a bunch of other non-borrow-checking-related features that trade off compilation speed for other desiderata (monomorphization, LLVM optimization, procedural macros, crates as a translation unit). Also because the rustc codebase is huge and fairly arcane and not that many people understand it well, and while there's a lot of room for improvement in principle it's mostly not low-hanging fruit, requiring major architectural changes, so it'd require a large investment of resources which no one has put up.
There are other legitimate criticisms you can raise at the Rust borrow checker such as cognitive load and higher cost of refactoring, but the compilation speed argument is just baseless.
> Lifetime annotations are checked at compile-time. ... This is the major reason for slower compilation times in Rust.
This misconception is being perpetuated by Rust tutorials.
Many-many people remarked that this is the wrong approach in todays world, where CPUs are good at doing dynamic dispatch prediction, but the cache sizes (esp. L1, and instr cache) is very limited, for most code (with the exception of very hot tight loops), fetching code into cache is going to be the bottleneck.
Not to mention, for a systems programming language, I'd expect a degree of neatness of the generated machine code (e.g. no crazy name mangling, having the same generic method appear 30 places in assembly etc.)
it's complicated, and simple
the simple answer is rust compiler times are not dominated by the borrow checker at all, so "it's fast" and you can say it's not overly related to their being borrow checking
the other simple answer is that a simple reasonable well implemented borrow checker is pretty much always fast
to complicated answer is that rusts borrow checker isn't simple as there are a _huge lot_ of code a simple borrow checker wouldn't allow which is safe and people want to write and the borrow checker rust needs to run to support all that edge cases has to basically run a constraint solver. (Which a) is a thing which in O notation is quite slow and b) is a thing CS has researched optimizations and heuristics for since decades so it is often quite fast in practice.) And as far as I remember rust currently does (did? wanted to?) run this in two layers, the simple checker checks most code and the more powerful on only engages for the cases where the simple checker fails. But , like mentioned, as the compilation still isn't dominated by the borrow checker this doesn't exactly mean its slow.
So the borrow checker isn't an issue and if you create a C-like language with a rust like borrow checker it will compile speedily, at least theoretically, if you then also have a tone of code gen and large compilation units you might run into similar issues as rust does ;)
Also recently most of the "especially bad cases" project in rust have run into (wrt. compiler times, AFIK) all had the same kind of pattern: A enormous huge amount of code (often auto generated, often even huge before monomorphization) being squeezed into very few (often one single) LLVM compilation unit leading to both LLVM struggling hard with optimizations and then the linker drowning, too. And here is the thing, that can happen to you in C too and then your compilation times will be terrible, too. Through people tend to very rarely run into it in C.
> not low-hanging fruit, requiring major architectural changes, so it'd require a large investment of resources which no one has put up.
it still happens from time to time (e.g. polenious) and then there are still many "hard" but very useful improvements which don't require any large scale architectural changes and also some bigger issues which wouldn't be fixed by large scale architectural improvements. So not sure if we are anywhere close to needing a large scale architectural overall in rustc, probably not.
E.g. in a somewhat recent article about way too long rust compiler times many HN comments thought that rustc had some major architectural issues wrt. parallelization, but the issue was that rust failed to properly subsection the massive auto-generated crate when handing code units to LLVM and that isn't an architectural issue. Or e.g. not replacing LLVM with cranelift (if viable) for the change->unit test loop is a good example for a change which can largely improve dev experience/decrease compiler times for the place where it matters the most (technically it does change the architecture of the stack, and needed many many small changes to allow a non LLVM backend, but it's not "a major rewrite(architectural) change" in the rustc compiler code).
Lexically scoped lifetimes don't address this at all.
What the C3 solution DOES to provide a way to detect at runtime when already freed temporary allocation is used. That's of course not the level of compile time checking that Rust does. But then Rust has a lot more in the language in order to support this.
Conversely C3 does have contracts as a language feature, which Rust doesn't have, so C3 is able to do static checking with the contracts to reject contract violations at compile time, which runtime contracts like some Rust creates provides, can't do.
Doing alias analysis on mutable pointers seems to be inevitable in so many areas of programming and Rust is just one of the few programming languages brave enough to embark on this idea.
This post is about memory management and doesn't seem to be concerned much about safety in any way. In C3, does anything prevent me from doing this:
fn int* example(int input)
{
@pool()
{
int* temp_variable = mem::tnew(int);
*temp_variable = input;
return temp_variable;
};
}Memory arenas/pools have been around for ages, and binding arenas to a lexical scope is also not a new concept. C++ was doing this with RAAI, and you could implement this in Go with defer and in other languages by wrapping the scope with a closure.
This post discusses how arenas are implemented in C3 and what they're useful for, but as other people have said this doesn't make sense to compare arenas to reference counting or a borrow checker. Arenas make memory management simpler in many scenarios, and greatly reduce (but don't necessarily eliminate - without other accompanying language features) the chances of a memory leak. But they contribute very little to memory safety and they're not nearly as versatile as a full-fledged borrow checker or reference counting.
(I have not actually checked the standard, but I'm reasonably sure pools were there.)
> But they contribute very little to memory safety [...]
They do solve memory safety if designed properly, as in Ada, but they're not designed in a way that does anything useful here. In Ada the pointer type has to reference the memory pool, so it's simply impossible for a pointer to a pool to exist once the pool is out of scope because the pointer type will also be out of scope. This of course assumes that you only use memory pools and never require explicit allocation/deallocation, which you often do in real world programs.
The blog claims that @pool "solves memory lifetimes with scopes" yet it looks like a classic region/arena allocator that frees everything at the end of a lexical block… a technique that’s been around for decades.
Where do affine or linear guarantees come in?
From the examples I don’t see any restrictions on aliasing or on moving data between pools, so how are use‑after‑free bugs prevented once a pointer escapes its region?
And the line about having "solved memory management" for total functions::: bravo indeed…
Could you show a non‑trivial case where @pool eliminates a leak that an ordinary arena allocator wouldn’t?
Could you show a non‑trivial case, say, a multithreaded game loop where entities span multiple frames, or a high‑throughput server that streams chunked responses, where @pool prevents leaks that a plain arena allocator would not?
This doesn't actually do any compile-time checks (it could, but it doesn't). It will do runtime checks on supported platforms by using page protection features eventually, but that's not really the goal.
The goal is actually extremely simple: make working with temporary data very easy, which is where most memory management messes happen in C.
The main difference between this and a typical arena allocator is the clearly scoped nature of it in the language. Temporary data that is local to the function is allocated in a new @pool scope. Temporary data that is returned to the caller is allocated in the parent @pool scope.
Personally I don't like the precise way this works too much because the decision of whether returned data is temporary or not should be the responsibility of the caller, not the callee. I'm guessing it is possible to set the temp allocator to point to the global allocator to work around this, but the callee will still be grabbing the parent "temp" scope which is just wrong to me.
But mentioning the borrow checker raises an obvious question that I don’t see addressed in this post: what happens if you try to take a reference to an object in the temporary allocator, and use it outside of the temporary allocator’s scope? Is that an error? Rust’s borrow checker has no runtime behavior, it only exists to create errors in cases like that, so the title invites the question of how your this mechanism handles that case but doesn’t answer it.
This is of course not as good as ASAN or a borrow checker, but it interacts very nicely with C.
One: I don't believe they have solved use-after-free. Marking memory freed and crashing at runtime is as good as checked bounds indexing. It turns RCE into DOS which is reasonable, but what would be much better is solving it provably at compile time to reject invalid programs (those that use memory after it has been deallocated). But enough about that.
I want to write about memory leaks. Solving memory leaks is not hard because automatically cleaning up memory is hard. This is a solved problem, and the domain of automatic memory management/reclamation aka garbage collection. However I don't think they've gone through the rigor to prove why this is significantly different than say, segmented stacks (where each stack segment is your arena). By "significantly different" you should be able to prove this enables language semantics that are not possible with growable stacks - not just nebulous claims about performance.
No, the hard part of solving memory leaks is that they need to be solved for a specific class of program: one that must handle resource exhaustion (otherwise - assume infinite memory; leaks are not a bug). The actual hard thing is when there are no memory leaks in the sense that your program has correctly cleaned itself up everywhere it is able and you are still exhausting resources and must selectively crash tasks (in O(1) memory, because you can't allocate), those tasks need to be able to handle being crashed, and they must not spawn so many more tasks as to overwhelm again. This is equivalent to the halting problem, by the way, so automatic solutions for the general case are provably impossible.
I don't believe that can be solved by semantically inventing an infinite stack. It's a hard architectural problem, which is why people don't bother to solve it - they assume infinite memory, crash the whole program as needed, and make a best effort at garbage collection.
All that said, this is a very interesting design space. We are trapped in the malloc/free model of the universe which are known performance and correctness pits and experimenting with different allocation semantics is a good thing. I like where C3 and Zig's heads are at here, because ignoring allocators is actually a huge problem in Rust in practice.
It’s also something allocators commonly implement already.
It doesn't solve the case when lifetimes are indeterminate. But often they are well know. Consider "foo(bar())" where "bar()" returns an allocated object that we wish to free after "foo" has used it. In something like C it's easy to accidentally leak such a temporary object, and doing it properly means several lines of code, which might be bad if it's intended for an `if` statement or `while`.
Now we usually cannot do "bar(foo())" because it then leaks. We could allocate a buffer on the stack, and then do "bar(foo(&buffer))", but this relies on us safely knowing that the buffer does not overflow.
If the language has RAII, we can use that to return an object which will release itself after going out of scope e.g. std::unique_ptr, but this relies on said RAII and preferably move semantics.
If the context is RAII-less semantics, this is not trivial to solve. Languages that run into this is C3, Zig, C and Odin.
With the temp allocator solution, we can write `bar(foo())` if `foo` always allocates a temp variable, or `bar(foo(tmem))` if it takes an allocator.
@pool appears to be exactly what C++ does automatically when objects fall out of scope.
(This doesn't seem to have anything to do with borrow checking though, which is a memory safety feature not a memory management feature. Rust manages memory with affine types which is a completely separate thing, you could write an entire program without a single reference if you really wanted to)
Also, since D lang usually implements all kinds of possible concepts and mechanism from other languages, I would love to see those being implemented aswell! D already has a borrow checker no so why not also add this, would be very cool to play with it!
https://www.gnu.org/software/libc/manual/html_node/Obstacks....
alloca
https://man7.org/linux/man-pages/man3/alloca.3.html :)It seems that @pool is providing context the the allocator function(s), but is the memory in the pool contiguous? What is the initial capacity of the pool? What happens when the pool needs to grow?
I think I prefer the explicit allocator passing in Zig. I don't need to ask these questions because I'm choosing and passing the allocator myself.
Operating systems free resources upon process exit. That's one of the fundamental responsibilities of an OS. You can use malloc without free all you want if you are just going to exit the process.
- integers use names like "short" instead of names with numbers like "i16"
- they use printf-like formatting functions instead of Python's f-strings
- it seems that there is no exception in case of integer overflow or floating point errors
- it seems that there is no pointer lifetime checking
- functions are public by default
- "if" statement still requires parenthesis around boolean expression
Also I don't think scopes solve the problem when you need to add and delete objects, for example, in response to requests.
So, you will still need a borrow checker for the same reasons Rust needs one, and C/C++ also needed.