5 ms·
A point that seems to get lost in many of the pro-GC articles, this one included from what I can see, is that memory is just one kind of resource. Correct code
by magicalhippo 3y ago
A point that seems to get lost in many of the pro-GC articles, this one included from what I can see, is that memory is just one kind of resource.
Correct code, especially in systems programming, will need to manage external resources as well, be it file handles, sockets or whatnot. GC only solves the application memory part, thus doesn't help at all for handling those external resources. In fact it can make it much more complicated, just look at what it takes to correctly implement a non-trivial IDispose in .Net.
Other approaches like RAII or reference counting makes it much easier in my experience to handle both memory and external resources in a unified way, thus making it easier to write correct code and reason about it.
That said, I'm not blatantly against GC's. It's a tool and it has some pros and cons, like everything else. The "manual GC" RCU approach mentioned in the article is interesting for certain tasks.
- zwieback 3y agoSo true, moving from C++ to mostly C# I'm liking memory management by GC but hating tracking file handles, sockets, etc. RAII is something I really appreciated
- neonsunset 3y ago(SafeFileHandle is internally reference counted, but yes, worst case when you forget to .Dispose it, it waits in the finalizer queue for the GC to trigger the finalizer thread to process the items in it)
- pjmlp 3y agoUse Roslyn analysis that errors when using is forgotten in an IDisposable type, for example. By the way, in modern .NET, using makes use of structural typing. Any class or struct, with a Dispose() method can be referred to, no need to implement the interface, and can also be retrofitted via extension methods.
- osigurdson 3y agoThis is strange advice. Why not just implement the IDisposable interface like normal C# code? Using extension methods for this is strange as well. Doing this even somewhat correctly would mean creating another class that implements IDisposable which can only work with public members of the original. In general best not to do weird stuff for no reason imo.
- pjmlp 3y agoBecause you don't own the type? Using another class, or struct adds up to memory usage, and turns out some C and C++ folks are itchy with such solutions, not to mention the added issue of passing wrapper classes/structs around.
- osigurdson 3y agoIf the hypothetical is: 1) you don't own the type, 2) it holds onto unmanaged resources, 3) the author didn't bother implementing IDisposable. Furthermore, 4) the unmanaged resources are publicly exposed. This seems like an extreme edge case but ok. As soon as you are done implementing it, please contact the author and ask them to implement IDisposable like every other class that holds onto unmanaged resources.
- pjmlp 3y agoI guess it goes to prove lacking the experience of working on big corporation code, delivered by external contractors.
- pron 3y agoThere is a big difference between memory and other resources, and that is that memory -- like processing -- is fundamental for any computation; not for nothing do most theoretical models of computation assume unbounded memory. Very few languages require manual allocation of processing -- even though that is done in some software like OS kernels and hard realtime applications -- and for similar reasons automatic memory management is very useful for abstracting computation, i.e. not leaking their details of memory by a subroutine to its clients just as this is rarely done for CPU use. So while every non-trivial computation involves some non-constant amount of processing and memory, I/O is usually done at the edges of the system. Management of I/O is very important, of course, but it's not as central to the notion of computation (and therefore to abstracting computation) as processing and memory.
- samatman 3y ago> GC only solves the application memory part, thus doesn't help at all for handling those external resources. This is far from entirely true. Most languages with a garbage collector have finalizers, which will clean that sort of thing up. Generally, and unlike memory allocation, one can call those finalizers from within user code, so as not to have to rely on the GC running. The distinction between reference counting and garbage collection is an artificial one. Reference counting is an approach to garbage collection, one with different tradeoffs from more concept-central algorithms for GC. I agree with you that it's a more unified approach to resource management, finalizers require user attention in a way which ordinary allocation doesn't, so yes, system resources get treated differently. I don't see that as a slam-dunk against them, however.
- o11c 3y agoOne problem with finalizers, try-finally, try-with-resources, Python-`with`, etc. is that they don't actually guarantee the code will be called even in common cases. In particular, any function that returns a (possibly newly-constructed) open file handle has not yet informed the caller that it's actually a thing to keep track of, unlike with destructors which are active immediately once the object's lifetime begins, and only rely on `move` being relatively atomic.
- kaba0 3y agoHow is a try-with-resources not guaranteed? At least in Java, it is guaranteed to run - plus, you can get your closing/destructor logic wrong in RAII languages as well.
- cesarb 3y ago> How is a try-with-resources not guaranteed? At least in Java, it is guaranteed to run There's a subtle detail you have to be careful with, however: if you try to allocate memory or call a method between allocating your resource and actually doing the try-with-resources, you might get an OutOfMemoryError or a StackOverflowError, and your resource will leak. That is, if you do something like: try (MyHolder holder = new MyHolder(allocateResource())) { ... } You can have a leak if the memory allocation in the "new MyHolder()" fails, or if there's not enough space in the stack to call the MyHolder constructor. > plus, you can get your closing/destructor logic wrong in RAII languages as well. For instance, in C++ you can accidentally put your resource in a temporary which is immediately destructed at the end of the line, when you wanted it to last until the end of the enclosing scope. Rust makes it harder for this to happen, but it's still possible.
- tialaramex 3y agoYes, and the "Memory Safety" arguments apply to the other resources too. For example Rust eventually grew I/O safety, so your File Handles (in Unix, OwnedFd) or just straight up Handles (in Windows, OwnedHandle) are owned objects, not just integers like the number 4 At the surface this looks like it's just about avoiding dumb mistakes like using arithmetic on handles or mistakenly using reserved values as sentinels -- but the ownership model also means anything tricky we're doing with handles has explicit ownership, transparent to future maintainers.
- pjmlp 3y agoYet, a point that tends to get lost when criticising GC languages, is that most of them have features for deterministic resource management, that many keep failing to learn. - Some do have RAII - Some do offer keywords - Some do arena like management, lambdas with implicit management - Some have a little help from the type system - Some do a bit of everything listed above Additionally, just like system developers have to rely on static analysers, the static analysers for those languages can also provide validation when something is forgotten, when the type system alone isn't enough.
- magicalhippo 3y agoMy point though is that by moving memory management into "don't care" territory while still, obviously, requiring explicit handling of other resources, it's easier to forget or miss when you need to explicitly handle something. When instantiating objects in C# say I need to check the documentation or the source code to see if it implements IDisposable to know if I need to handle it. Lets say that for a given class X in library Y, it does not. So I don't worry, I just instantiate and don't do anything about the cleanup because GC will handle it. Later, the implementation of X changes and IDisposable is added to it. I now have to change my code, and not doing so could lead to serious production issues. Yet the compiler happily compiles my code without any warning. Sure some static analyzers might catch it, but they're not perfect, and you need to run them. A stock Visual Studio 2022 will not complain about the above scenario for example. In my experience it's much less error prone to unify memory and external resource management. If instead I had been using a different language which has a more uniform resource handling, the above change in class X would likely not have been a big deal. Also, my code will already be written with resource handling in mind. It can be non-trivial having to change a hierarchy of classes just because a dependency deep down suddenly had IDisposable added to it. I guess what I'm trying to say is that I think just focusing on memory management, that is to GC or not to GC, is having a myopic view things. I feel it's like arguing what kind of pickle to have on your burger without considering the other ingredients. Sure, the pickle is a crucial ingredient, but there's a lot more to it.
- pjmlp 3y ago
- worik 3y agoYes. But... > GC only solves the application memory part, Except it does not. "Solve" that is. It helps, yes it does But as I have learnt the hard way (silly way too, to be truthful) GC will not help unless you actually delete all references to the unused memory Perhaps GC have developed magic moo cow properties in the twenty five years since I made that discovery, but I think the point remains GC is very helpful, but it does not stop resource leaks
- erik_seaberg 3y agoIf unreachable objects point at each other, that's only a problem for refcounting, not for tracing (their pointers aren't followed).
- pebal 3y agoHaving garbage collection (GC) in your toolkit doesn't mean you're limited to choosing between GC and deterministic destruction. You can integrate GC, stack allocation, and manual memory management within the same application. It's possible to leverage GC in a manner similar to how `shared_ptr` is used, providing both automated cleanup and precise control over object lifetime. For a practical example, consider SGCL: https://github.com/pebal/sgcl https://github.com/pebal/sgcl
- rbehrends 3y agoFirst, I have no desire to handle both memory and external resources in a unified way, because memory management and resource management have different needs. Memory is not just "one kind of resource", it's a very specific type of resource that if it has to be managed manually, inherently creates crosscutting concerns. And memory allocation is pervasive, often implicit in other language constructs. Garbage collectors get to cheat here, because they have a global view that ignores module boundaries and information hiding. The classical example is that introducing a caching mechanism usually introduces API breaks. Where a function normally returns a pointer/reference/unique pointer and makes the caller responsible for freeing memory (whether through convention such as in C or enforced/automated through language mechanisms such as in Rust), the moment you cache it, you need to return a reference-counted pointer, because now the memory can only be freed if both the caller and the cache don't use it anymore. And that change from a non-reference-counted pointer to a reference-counted pointer is a breaking API change. There are plenty more situations where manual memory management interacts poorly with modularity, such as filter() style functions, or the various complications that arise from closures capturing their local environment. Conversely, it is absolutely possible to have pretty straightforward resource management with guaranteed and predictable lifetimes in a GCed language (though, alas, there's a lack of direct language support for that). The general approach is as follows: Each resource's constructor takes an explicit or implicit owner argument (implicit being the current scope, whether defined through a language construct). You can also transfer a resource to a different owner (reparenting) [1]. Owners of resources can be lifetime managers such as scopes (but those do not need to correspond to lexical scopes and are more like transactions), that have more complex lifetime logic (such as a pool of resources) or objects that themselves are owned (e.g. if you have resources dependent on other resources). When the lifetime of the owner finishes, it calls a dispose function in all owned objects. Because an owner is required in order to construct such a resource object (unlike a C# using clause or Java's try-with-resources) by virtue of its constructor requiring it, it is impossible to accidentally create such a resource without a controlled lifetime [2]. Note that this is not the equivalent to RAII. You can have a number of non-owning references to such resource objects, essentially the equivalent of a weak pointer. In my experience, this is generally a good thing, because you do not want to have a hidden pointer secretly extending the lifetime of a potentially expensive resource. I prefer resource lifetimes to be explicit and to get an error if they are used past their intended lifetime. [1] Note that this is conceptually similar to talloc. https://talloc.samba.org/talloc/doc/html/index.html https://talloc.samba.org/talloc/doc/html/index.html [2] Obviously, it is still possible in any language to do the equivalent of a raw fopen() call, but that's not something that RAII can fix, either.
- yawaramin 3y agoRAII is of course great but any language with a GC and proper exception handling can handle resources safely. Eg look at Java's try-with-resources statement which guarantees that the resource will be disposed safely if an exception is raised: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html https://docs.oracle.com/javase/tutorial/essential/exceptions... You can build up quite resilient and resource-safe systems using these basic building blocks.