5 ms·
This has been around for a long time. The problem is, GC is not just something you can bolt on, it's a fundamental part of the language that colors everything y
by kentonv 11y ago
This has been around for a long time. The problem is, GC is not just something you can bolt on, it's a fundamental part of the language that colors everything you write -- and it fundamentally clashes with the way C++ is normally written.
In particular, C++ (and Rust, etc.) stresses RAII (Resource Acquisition Is Initialization), in which you write classes with destructors that cause the object to release its resources as soon as it goes out of scope. "Resources" here often means heap memory, but can also mean a whole lot of other things (file handles, child processes, network connections, arbitrary resources on remote network services, etc.).
In GC'd languages, the GC really only handles heap memory. Yes, you can sometimes rely on finalizers to free other kinds of resources, but generally you will find this doesn't work out well: most GC algorithms do not make any guarantees about how soon an unreachable object will be cleaned up, and they tend to use heuristics that assume that the cost of not cleaning up an object is exactly whatever RAM it is using, nothing more. Often, if your process is not allocating any new objects, the GC will never run at all.
Thus in GC'd languages you often see classes with methods like `.close()` or `.dispose()` for eagerly releasing non-local resources. These resources have to be managed manually, just like heap has to be managed in non-GC languages. The beauty of RAII is that you can use it to manage any kind of resource, but GC languages don't typically support RAII because it does not mix well with GC semantics.
These differences completely change how APIs in these languages are designed. Essentially, adding GC to a language -- or removing it -- gives you a different language that happens to share some syntax, and probably not a very good language since all that syntax was designed around a fundamental assumption that no longer holds.
I kind of wonder if a preference for GC languages is in part responsible for the way distributed systems are typically designed these days -- namely, the emphasis on stateless servers, which avoids the need to do remote resource management (which is hard without RAII), at the expense of requiring a lot more machinery to make performant (e.g. complicated caching layers because all state changes have to hit storage which is sllloowwwwwww).
- pjmlp 11y ago> The problem is, GC is not just something you can bolt on, it's a fundamental part of the language that colors everything you write -- and it fundamentally clashes with the way C++ is normally written. C++ and C yes, because these are systems languages where everything goes, which make impossible to apply certain classes of GC algorithms. Apple's failure to make one work in Objective-C is proof of that. However there were systems programming languages with GC, like Cedar, Oberon and Modula-3. Just like C++, with value by default, stack allocation and since they are safe by default, requiring explicit unsafe blocks, their GCs were better than anything C or C++ can ever get. Still, ANSI C++11 did get a GC pluggable API. > these resources have to be managed manually, just like heap has to be managed in non-GC languages. Only if the programmers are too lazy to learn how to make use of using/try-resource/defer/scope, or higher order functions. But GC critics always fail to acknowledge these language features.
- kentonv 11y ago> using/try-resource/defer/scope, or higher order functions Those only cover cases where your resource's lifetime matches a stack frame's lifetime. It doesn't cover the case where your resource's lifetime matches the lifetime of some owner object. For that you need destructors, by definition. Ownership transfer is also very hard to model robustly unless you have linear types / move semantics (which even C++ didn't really have until C++11).
- pjmlp 11y agoYou can map them into higher level constructs that basically are a kind of reference counting on top of GC. For example, multiple threads sharing a socket connection. Just create a barrier and have a cleanup thread closing the socket when everyone joins. Similar patterns can be applied to other resources. Ownership transfer is usually not an issue in GC enabled languages in terms of data structures, specially if every thread keeps the OS resources locally for itself.
- pcwalton 11y ago> You can map them into higher level constructs that basically are a kind of reference counting on top of GC. > For example, multiple threads sharing a socket connection. Just create a barrier and have a cleanup thread closing the socket when everyone joins. Nobody's saying you can't do it. But RAII makes it way easier. When you implement reference counting manually, you'll find that you want something like a copy constructor, so you remember to bump the reference count every time a new instance of the object is assigned. Then you find that you'll want the compiler to automatically drop reference counts when the object goes out of scope, so you don't forget. Then you'll find you want move semantics, to allow transfer of references without adjusting the counts. You'll discover you want the correct behavior in the presence of exceptions. In short, you want RAII.
- pjmlp 11y ago> Nobody's saying you can't do it. But RAII makes it way easier. Except RAII doesn't work for heap allocated object and then it is back to reference counting. > You'll discover you want the correct behavior in the presence of exceptions. In short, you want RAII. Well, then one would have implemented a GC by hand with worse performance, specially in multi-threading contexts, unless there is someone on the team expert in hazard pointers and related performance tricks. Maybe a performance improvement would be for the ANSI C++ standard to mandate that that compilers give special treatment to *_ptr<>(), so that they can elide needless calls, but I don't see it happening. In FP languages one can use the concept of transactions, yes the M word, to encapsulate scope based workflows where the resources are to be released at the end. What I can definitely see killing GC is affine types becoming mainstream, but they need to be made more friendly for the average Joe developer. Rust made them easier than ATS or Cyclone, but still a bit hard. I think ParaSail's work might also be worth checking in this regard.
- amelius 11y ago> In particular, C++ (and Rust, etc.) stresses RAII (Resource Acquisition Is Initialization) But there is no reason that the RAII pattern couldn't be completely orthogonal to memory management.
- ExpiredLink 11y agoCould. A hypothetical new language could be created to facilitate whole-program escape analysis and thus completely avoid both GC and manual memory (resource) management.
- Others 11y agoThis sounds awfully like the ownership concepts used in Rust...
- moonchrome 11y ago>But there is no reason that the RAII pattern couldn't be completely orthogonal to memory management GC + destructors = very very messy even in languages built with GC in mind, kentonv described the whole non deterministic finalizers nicely but there are also other problems see here : http://ericlippert.com/2015/05/18/when-everything-you-know-is-wrong-part-one/ http://ericlippert.com/2015/05/18/when-everything-you-know-i...
- mlvljr 11y ago>> In particular, C++ (and Rust, etc.) stresses RAII (Resource Acquisition Is Initialization) And the new Perl, they say
- kentonv 11y agoYou could do that, but if you are using RAII, then GC has lost a lot of its value. It is incorrect to access an object after its destructor has executed, so you might as well release its heap resources then and ditch the whole complicated GC. I suppose in theory you could use GC just for objects that (transitively) don't hold any non-heap resources. But that gets a little weird as if you ever device that you need to stick a non-heap resource in there, suddenly all your code needs to change. (Of course, this is already generally the case in GC languages...)
- 72deluxe 11y agoThanks for this very informative comment. I had not thought of "close()" and "dispose()" functions as the manual operations equal to manual memory management, but on reflection this is what it boils down to.
- KayEss 11y agoThe GC won't do anything for stack allocated objects so you can still use the RAII goodness for all of those. You just don't also need to use things like `std::unique_ptr` and `std::shared_ptr` to manage heap allocated objects.
- koffiezet 11y agoWhile in C++, RAII is a good practice, it has it's fair share of problems you should not ignore. Debugging, profiling and unit testing are a bitch. Performance becomes completely unpredictable in more complex applications. And then error handling.... Destructors shouldn't throw exceptions - and you don't have any other choice than just catch them all and 'log and forget' - which can be seriously problematic. Both RAII and the explicit cleanup of resources have their advantages and serious disadvantages. In C++ you better use RAII, in GC'd language, use explicit cleanup. What is better depends on the language. > I kind of wonder if a preference for GC languages is in part responsible for the way distributed systems are typically designed these days -- namely, the emphasis on stateless servers, which avoids the need to do remote resource management (which is hard without RAII), at the expense of requiring a lot more machinery to make performant (e.g. complicated caching layers because all state changes have to hit storage which is sllloowwwwwww) Don't wonder, they're not related at all. We have a few stateless services running that are written in C++. They are the only sane option once you start working beyond a certain scale - which is the point when you decide you need a distributed system. The reason for them being stateless is simple: nodes WILL fail. Hardware failures, power outages, line cuts, ... This is not the reason for 'more machinery'. Resource cleanup is hardly a bottleneck? RAII does this for you, but the only difference with GC'd languages is that you need to do this cleanup yourself. There are other reasons for having to add 'more machinery' to make things performant: going for 'easy' scripting languages to get things up and running in record time - which is important for startups to have something to show for as quickly as possible. Performance at that moment takes a back-seat, and it's hard to go back. Micro-services are becoming a trend for this reason too - you can quickly write your services in <slow hipster scripting language>, get everything up & running, identify your bottlenecks and optimize them. Just look at the upcoming trend there for 'optimizing' distributed applications is to rewrite parts of your service in Go, which is a native compiled GC'd language. Easy to write, little memory overhead with decent execution speed, and designed to do concurrency well. Does it compete with C++? Not at all, but it gets things done that would be pretty damn annoying to do in C++ - and it does it with little memory and CPU overhead. That - or going for some Java enterpricy application with tons of frameworks which eats up all your memory. Hello 32-core machine with 96Gig ram to generate some silly reports.
- 11y ago
- rbehrends 11y agoI'm not a big fan of RAII because it falsely conflates lifetime with reachability. This assumption breaks under a number of circumstances, e.g.: * Closures capturing their environment and keeping local variables alive longer than intended. * Debugging code keeping a record of objects. In either case, you have likely failure modes in that either the resource can be deallocated prematurely and then accessed through a stale reference or that it is kept around longer than its intended lifetime. RAII also does not provide a solution to a number of resource management scenarios, such as requiring access to an unbounded number of resources of which only a finite number can be reified at any time (e.g. due to the maximum number of file handles a process can manage). The obvious solution – a caching mechanism that will disconnect a resource from its in-memory representation upon eviction and reconnect when it is being used again – needs to attach itself to resource usage, not resource creation and destruction. Most scenarios where RAII is being used are either trivially handled in any language that supports closures or macros, or benefit from explicit reasoning about resource lifetime, anyway. Speaking of closures, I think it's no coincidence that RAII first arose in a language that until recently had no such concept; closures both create problems for RAII and also obviate many common use cases.
- mlvljr 11y agore: * Closures capturing their environment and keeping local variables alive longer than intended. How can that happen to a stack variable (and RAII is about those), given C++11/14's closures ("lambdas") do not prolongate the lifetime of the objects captured by reference? Or did you mean capture by value? (would be really confusing with RAII-ing classes :) )
- unscaled 11y agoProperly executed RAII, at least in C++, should never break in the two cases you mention. The RAAI object should be non-copyable (no copy constructor in C++), so the only way it can be passed by value to a closure is with an explicit move operation, which is not something you would do on mistake. The original copy would now revert to a proper "uninitialized" or "closed" state. Debuggers too are usually well aware of out-of-scope objects, so I don't see the point here. As for complex scenarios, they are probably less than 1% of the time you'd use RAAI. When you do have complex scenarios that need techinques like caching and pooling - well, yeah, RAAI won't solve them on its own, but the same goes for macros and closures. Besides that, most RAAI uses just can't be replaced by closures, since you cannot break a loop or return a value from within a closure, for instance (Ruby procs can do that, but there's no equivalent to that in most languages). And both a closure-based and a macro-based solution quickly become an unwieldy nest-fest once you need to deal with multiple resources in a row.
- cousin_it 11y ago> don't typically support RAII because it does not mix well with GC semantics Just to expand on this a little, one problem is that no matter how you collect a cycle of objects with finalizers, one of them will see the others in a finalized state. Another problem is resurrection, where the finalizer creates a new reference to self or another finalized object. You could try solving these problems by nulling all references to finalized objects (including self) during GC, but that makes finalizers useless. I don't know any good solution.
- white-flame 11y agoLisp does the equivalent of scope-level RAII with macros. For instance, (with-open-file (myfile "foo.txt") ...body...) will execute the body with the opened file bound to variable MYFILE, closing it when the end of the body is reached (or is unrolled away). Many libraries expose this type of interface, and by Lisp's use of dynamic scoping, nested functions within the scope can access the current state of that particular scoped resource without having to pass it around manually. In effect, RAII is programmer discipline which can manually repurpose a particular object allocation/deallocation style to support scope behavior, while languages like Lisp can simply work with scope behavior directly.