4 ms·
Yeah sure it's problematic. For instance, if you have a deeply nested struct, and you have to add a lifetime somewhere in the bottom, now you have to propagate
by skohan 6y ago
Yeah sure it's problematic. For instance, if you have a deeply nested struct, and you have to add a lifetime somewhere in the bottom, now you have to propagate it all the way up to the top level. Also if you run into a case where you need an explicit lifetime in a function somewhere, this ends up propagating outward.
I've run into cases with Rust where I refactored a significant portion of a program to avoid nested references and explicit lifetimes in a way I haven't had to in other languages
- fauigerzigerk 6y agoYes, but you only have three choices basically: a) Accept the performance hit that comes with GC or defensive copying. b) Convince yourself that developers can keep all those potentially dangling references in their heads at all times. c) Use something like Rust lifetimes. My preference would be (a) whenever possible, (c) when absolutely necessary, and never (b)
- zozbot234 6y agoYou don't need GC or copying. Rust supports reference-counted handles that will never dangle, and will only leak in very rare cases involving self-referential data structures.
- fauigerzigerk 6y agoI consider reference counting a form of GC - a rather slow one with lots of potential pitfalls.
- nickez 6y agoIt might be slow, but it is deterministic which is more important in systems programs.
- Joker_vD 6y agoIn what sense it is deterministic? Any refcount decrement may or may not be the last one and that may or may not cause a huge graph of objects being free()d which (even without destructors) takes unpredictable amount of time. Then again, malloc() itself is usually not deterministic in a sense that it may take an arbitrary amount of time to make an allocation: many implementations shift work from free() to malloc() to get better asymptotic time complexity.
- pkolaczk 6y agoThe problem of destructor chains has been solved a long time ago. You can deallocate on another thread or deallocate in small batches during allocations. Also pauses from malloc/free are orders of magnitude smaller than pauses caused by tracing GC. Low pause tracing GC creators make it a big news when they manage to get pauses below 1 ms, while malloc/free operate in range of tens or maybe hundreds nanoseconds. Tracing GC can pause a fragment of code that does not make any allocations. Refcounting can't. Refcounting has only local effects - you pay for it where you use it. But when you add tracing GC, its problems spread everywhere.
- ghusbands 6y agoDelayed destruction (either on another thread or batches) removes the possibility of safely using RAII via finalizers, which removes one touted advantage of refcounting. Most modern implementations of refcounting end up having a cycle collector, anyway, which also means you can end up unexpectedly paused, removing another touted advantage. There are always trade-offs, so you can't really call any of it solved or make absolute claims. In general, GC implementations (including initially-refcounting) tend to converge over time in both performance and features.
- pkolaczk 6y ago> Delayed destruction (either on another thread or batches) removes the possibility of safely using RAII via finalizers No, it doesn't in general. Delayed destruction can be used selectively only for these object graphs that could cause large batches of deallocations, and only once you find by profiling that you really have a problem. Most object graphs are not like that, so don't penalize the average case for the edge case you might even not have at all. You can still use synchronous deallocation on the objects where immediate deallocation is needed. Secondly, I bet delayed deallocation on a background thread happens a lot sooner than waiting for the next GC to happen and to call the finalizers (which may never happen). With delayed deallocation the system knows how much there is to deallocate. With tracing GC there is no such knowledge until tracing is over. > Most modern implementations of refcounting end up having a cycle collector, anyway, which also means you can end up unexpectedly paused, removing another touted advantage. That's why Rust doesn't have a default cycle collector. Reference cycles are IMHO a non-issue in practice. There is no point in paying for cycle-collection by unpredictability of the whole program, when it helps only with edge-cases. > In general, GC implementations (including initially-refcounting) tend to converge over time in both performance and features. I'd frame it differently: all-purpose one-size-fits-all memory management solutions are almost universally suboptimal and easily beaten by solutions which give more control in programmer's hands. Rust is about providing that control about fine details and giving you a set of handy tools. The toolset might be slightly harder to learn initially due to borrow checker and lifetime rules, but over time I didn't notice any productivity hit from that (actually quite contrary, but this is a different topic). And somehow, even without me doing any heavy optimisations, my Rust programs tend to use 100x less memory and perform way faster than my Java programs. How can it be explained, when I have much more Java experience than Rust experience?
- pkolaczk 6y agoA "rather slow" is quite debatable. Tracing GC gets speed advantage at the cost of much higher memory and energy usage, unpredictable pauses, cache thrashing, etc. If you try to make these things equal, tracing can become embarrassingly slow, or even just crash with OOM.