One thing I can't seem to find a quick answer to from the Rust crowed is how does Rust handle dynamic lifetimes? It seems like it does not; it simply prevents you from referring to objects that have a dynamic lifetime not known at compile time.
You either have to use 'unsafe' or use the 'handles' pattern where you have what amounts to a custom allocator but the compiler does not know that it's an allocator so it can't prevent 'use after free' bugs.
I suppose you can also use Rc, but isn't that essentially a garbage collection scheme, with similar performance characteristics? (AFAICT a reference counting scheme must be able to nullify all references to an object when it's deallocated, so deallocations can become arbitrarily expensive).
- Use a Vec, slotmap, or a similar data-structure and store handles (indexes). This doesn't entirely prevent use-after-free bugs. But it does tend to make them panics rather than silent errors.
- Use unsafe (and ideally create your own higher-level safe abstraction)
In theory, there should also be 4:
- Use a GC library
But I haven't seen a good implementation of this yet.
---
If you're missing something, it's perhaps that most lifetimes aren't dynamic. So you get all the benefits of Rust's ownership in the 80% case. And in the 20% case you don't get any particular benefits, but you still have all the same options available to you as in other languages.
But most of the bugs I often face and seem to struggle with are with dynamic lifetimes (in graphics / simulation / gamedev). So even as a relatively low-level systems dev I don’t really find Rust’s borrow checker that useful.
I think there are cases where borrow checking is difficult for a domain, but without a lot more detail it's hard to say. My understanding is that graphics/gamedev is one of those cases and if you aren't willing to use ECS you may have problems.
You can also check index out of bounds in C++ code, and you actually have more freedom to control it in C++. In Rust bounds checks are required unless you use unsafe, in C++ you can turn it on/off freely depending on your usecase (for example, when you are in debug mode vs. deploying your code).
> AFAICT a reference counting scheme must be able to nullify all references to an object when it's deallocated, so deallocations can become arbitrarily expensive
The idea of reference-counted object is that by the time it's deallocated there _are_ no references left to update. Now you might reasonably be asking about the memory left behind by those old references which contain now-invalid pointers. The use-after-free risks of any single given reference to the object is something the Rust compiler can reason about quite easily. Once you drop one of them (and the refcount decrements) you are guaranteed not to be able to get at that pointer again; not until that memory is reinitialised with something else valid.
Well, there's a concept of a _weak_ reference. I don't know whether it exists in Rust but I can't imaigne a modern reference counting system without support for it.
A weak reference does not prevent the object from being deallocated, but the system guarantees that when the object is deallocated, all weak references to it will be nullified.
This is what makes deallocating objects in a refcounting system potentially arbitrarily expensive.
> A weak reference does not prevent the object from being deallocated, but the system guarantees that when the object is deallocated, all weak references to it will be nullified.
> This is what makes deallocating objects in a refcounting system potentially arbitrarily expensive.
Depends how you implement weakrefs. In Rust, when you upgrade a weakref it checks if there are outstanding strong references, and if there are not the control block is considered invalid and the upgrade fails.
There is no need to touch any of the weakrefs at any point.
And unlike languages like Python, Rust does not have weakref finalizers either.
Deallocation is still arbitrarily expensive in general, as the entire subtree will get deallocated recursively, but that has nothing to do with refcounting.
The way it works is, in order to use it you try to upgrade it to a normal Refcounted pointer. Since it is a weak reference, this upgrade may of course fail and in that case return None (in place of a null pointer). When this upgraded pointer dies (either goes out of scope or is manually downgraded) the refcount will again be updated.
Yes, your options are unsafety, garbage collection (in the broad sense, which includes Rc), and hiding lifetimes from the compiler via "handles" or similar.
Can you even imagine other options, though? If enough information about the lifetimes isn't known at compile time, how can the compiler prove it safe?
It's very rare that a serious problem can be solved without some objects having some sort of a dynamic lifetime that you have to manage.
Rust just prevents you from doing this. So you can say it provides safety, but it also restricts you from solving the vast majority of serious problems that we write programs to solve.
This is very different from static vs dynamic typing.
In a statically typed language, the compiler actually _knows_ what you can and can't do with each object.
Where as Rust's life time management just says "Nope, I have no idea whether what you are doing is safe or not so I'm just going to tell you you can't do it".
If you refer to heap allocations, then you can use Box, Arc, Rc. They are not a "garbage collector" nor do they incur performance hits other than a regular heap allocation.
yes, but that's not a JVM-like slowdown. It's fairly well amortised and _clearly_ not a matter of consideration when writing an app or a library.
If your issue is "atomics are slowing down my app", then I assume you already milked the code to the latest micro second of performances everywhere else. This is likely not the case here and not a general advice I would give to anyone.
Remember, some dev in python where concurrency is inexistent, start-up time is horrendous and performances are abysmal compared to rust. (this is exaggerated: of course you can run stuff in parallel in python)
Which should be incredibly negligible unless you're counting literal millions of objects. Even then, I'm suspecting that refcounting will never be in the top spots of things that slow you down.
What I'm saying is that bumping atomic references inside a hot loop will be detrimental for performance, and since it invalidates a cache line and this can flush caches for lots of cores. It also shuts down ILP.
One really nice thing about Rust's use of Arc is that you don't need to bump atomic references in a hot loop, or much at all usually.
Once you have an Arc<T> you can `as_ref()` to get a &T. So maybe you need an Arc to share something with another thread, so you clone it once. Once you're in that thread though you can go back to just using `&` and never touch the atomic again.
Technically yes, but if you only use it when you need it, it's often not a performance concideration.
And for systems programming, the often overlooked truth is that people care far less about perfect performance than they do about control - ref counting doesn't give up control to a mysterious oracle running in the background which may or may not wreck your performance in hard to predict ways, it just pays a known cost at the time you use it based on how you're using it. (that's not to say performance is irrelevant, but it's not always the top concern, and with control you can always rewrite slow code as needed).
The obvious question is "why can't we have an optional modern garbage collector built into a systems language?", and it's a good question (I remember reading there was one in rust for a while during early development, but it got removed), I think the main reason is that high quality garbage collectors are incredibly complicated, with many trade offs, and a gc that combines well with the rest of a language and various alternative memory tracking solutions is harder than most. The projects that really want one can always implement their own and choose their own tradeoffs, so there's not many use-cases where a generic language-provided one would justify the complexity of implementing it within the language.
> why can't we have an optional modern garbage collector built into a systems language?
It's possible in C#. Some language and runtime features like lambdas insist on using the GC, but with some care they can be avoided. The usability becomes worse without these features, but IMO that's not a dramatic downgrade.
Many pieces of the standard library in modern .NET don't require managed heap. Instead, they operate on stuff like Span<byte> which can be backed by anything: unmanaged heap, native stack, or even the memory mapped to user space by a Linux device driver (I did it with DRM, V4L2 and ALSA devices).
That's an issue when every object is managed. Which is not the case in an unmanaged language, you'd only refcount what you need to refcount.
Furthermore Rust can also safely borrow from a refcounted pointer without the need for refcount traffic, which can be quite the performance gain for atomic refcounts (it's nigh irrelevant for non-thread-safe refcounts as those just do a local increment/decrement).
It's generally true to be fair, reference counting could be used for every garbage collected language (and be much simpler). The only reason they switched to more complex schemes is they're faster on average. Even smart schemes that try to remove unnecessary ref count changes will tend to underperform compared to a (well built) tracing GC. As for a reference, https://en.wikipedia.org/wiki/Tracing_garbage_collection#Per...
The point about predictability is totally valid though (and combined with simplicity is the reason many languages still pick ref counting).
One thing I can't seem to find a quick answer to from the Rust crowed is how does Rust handle dynamic lifetimes? It seems like it does not; it simply prevents you from referring to objects that have a dynamic lifetime not known at compile time.
You either have to use 'unsafe' or use the 'handles' pattern where you have what amounts to a custom allocator but the compiler does not know that it's an allocator so it can't prevent 'use after free' bugs.
I suppose you can also use Rc, but isn't that essentially a garbage collection scheme, with similar performance characteristics? (AFAICT a reference counting scheme must be able to nullify all references to an object when it's deallocated, so deallocations can become arbitrarily expensive).
Am I missing something?