This is the first of what may become a small series of posts on advantages of garbage collection. I am not a person who sees garbage collection is the ideal system of memory management for all applications, but I see a tremendous amount of FUD about garbage collection in the public newsgroups, mostly coming from people who clearly have very little experience using it. Since the disadvantages of garbage collection are widely, if inaccurately, discussed, I’d like to shed some light on the other side of that coin.
For me, be general theme of most of the advantages of garbage collection can be summarized as "expressive programming." This means that your source code can be written to express the concise nature of the problem instead of precisely how to solve it. Most programmers do not write applications to solve the problem of memory management. Memory management, instead, is a kind of tax that we have to pay to get to our solution. Eliminating explicit memory management can allow the programmer to focus most directly on the problem at hand. As a loose analogy, consider the difference between a LINQ query and a traditional for loop. The former asserts the need to iterate, but delegates the method of iteration to the instance to be iterated. The traditional for loop, on the other hand, spells out the precise method of iteration. The LINQ query, therefore, is more expressive, since it does not pollute the description of what needs to be done with an algorithm for how it is to be done. The difference may seem kind of academic, until you examine how easy it is to parallelize the more expressive code. Then you see the advantage of expressive coating.
Expressing this point another way, manual memory management tends to force programmers to write their code in a way which suits the memory management as well is the problem instead of a way which most directly suits the problem. The most dramatic example of this is functional programming, a powerful technique which is extremely difficult to do well without garbage collection.
Garbage collection is often contrasted with "manual" memory management, as though any application which doesn’t use garbage collection frees all allocations with an explicit call to Free. But that isn’t true at all. In fact, Delphi has several different methods of memory management, depending upon what you’re doing. Off the top of my head:
- You can free an allocation with an explicit call to Free.
- Components can be freed via the Ownership mechanism. This is critical to streaming.
- Components will be freed when their Parent is freed.
- Some types are allocated on the stack, and freed when they go out of scope.
- Some types are reference counted.
Now, that’s a lot of different methods of handling memory, and I’m reasonably sure that I have not had all of them. Worse, they can and do conflict. This is most commonly noticed when using interfaces, and though it’s hardly the only example, it is perhaps the one with the greatest potential to cause problems. So much so, in fact, that I believe it is principally responsible for the fact that interfaces are not widely used in Win32 Delphi code (in contrast, for example, with .NET code), even though they were introduced in Delphi 3.
For just one example of how this conflict between memory management types complicates Win32 programming in Delphi, look closely at the constructor for TXMLDocument. Another is the lengths that you must take to ensure that you explicitly nil all interface references to a non–reference-counted instance before Freeing it, or you will almost certainly see an Access Violation in _IntfClear.
People occasionally suggest that reference counting is a holdover from COM, and a solution to this problem would be a non–reference-counted interface type. It’s not a bad idea in its own right, but it doesn’t really solve the problem of memory management. For various reasons, interfaces and reference counting are useful for any cross language interaction, not just COM. But even without cross language interaction, some kind of automatic memory management is still useful when using interfaces. The nature of how interfaces work is well-suited to garbage collection. Given an interface reference, you’re not really supposed to do low-level operations on the instance. Moreover, because of the limited window into the instance but an interface provides, it is the rule rather than the exception, when working with interfaces, but an application will have multiple references to an instance. If a particular class implements IFoo and IBar, you will very likely have one reference of each type at some point in the execution of the program. This is in contrast to object references, where it is more common to have a single reference to an instance, since the type of that reference probably does everything you need.
Contrast the five different types of memory management I’ve listed with a much simpler scenario for Delphi for .NET. There, you have only garbage collection of the heap and stack-based allocations, and they never conflict. Your code focuses on the task at hand, rather than when and how to release each instance you create.
Since a few people seem to read any assertion that garbage collection could possibly have any single redeeming value as a threat to their programming worldview, I will restate that this post is not an assertion that every programmer should give up manual memory management for all tasks, forever. Rather, it’s a description of a single feature that I like.
{ 9 } Comments
"Most programmers do not write applications to solve the problem of memory management. Memory management, instead, is a kind of tax that we have to pay to get to our solution. Eliminating explicit memory management can allow the programmer to focus most directly on the problem at hand."
Very well said!
Craig,
Thanks for that. That’s one blog post I don’t need to write now as it sums up why I like a gc in some scenarios.
I’m sorry, but to me it is a lot of loose sand heaped together.
Deterministic releaseing of an interface could be a perfect solution for what you suggest (one object, multiple references, allow object to destroy itself). It is not a case for GC IMHO.
Marco, that’s precisely the point I addressed in the post regarding interfaces. I believe that what you propose both violates the spirit of what an interface *is* (look at just about any implementation of interfaces anywhere), and also that something guaranteed to create dangling pointers doesn’t represent a reasonable solution to the problem of dangling pointers.
"Most programmers do not write applications to solve the problem of memory management. Memory management, instead, is a kind of tax that we have to pay to get to our solution. Eliminating explicit memory management can allow the programmer to focus most directly on the problem at hand."
This is like saying that structural integrity is a kind of tax that an architect has to pay to get a building that works well and is pleasing to the eye.
I wouldn’t want to live in a house built to a plan drawn up by someone who couldn’t be bothered with thinking about load bearing.
This business about "focussing on the problem, not on the details of the implementation" is the same nonesense that comes around every 10 years or so - applications built by people who understand the business problem, and every time it fails because the real "problem" is how to represent the business problem to a computer.
That requires a translation from the real world business problem into a problem that can be expressed in software and an appropriate solution.
i.e. it requires an implementation, and that requires details.
Neglecting the details is attractive at first and costly in the long run.
Any programmer who writes their code exclusively in 1s and 0s is welcome to lecture me on how the source code for an application must contain every single detail of how the program will arrive at its result. But I really doubt that anyone reading this has given up compilers. Most serious arguments for this nonsense went away when John Backus invented FORTRAN, but a few individuals still cling to the notion that one or two specific areas wildly diverge from the general rule that the greater the percentage of your code dedicated to overhead not directly relevant to the purpose of the application, the greater the chance that that code will be incorrect, and the greater the effort required to maintain it. The fact that we do have to manage a good deal of overhead today has more to do with the tools at our disposal than with the correctness of the solution.
So let’s not pretend that anyone doing production software development today is not "focusing on the problem rather than the details of the implementation," at least insofar as it’s possible to do so with present tools. Certainly, if you’re using Delphi, you do that every time you write code. If that’s "nonsense," then why stop with heap management? Can you *really* trust the compiler to manage the stack correctly?
Again, consider the example of looping in the post. The traditional for loop is not only less expressive, it is, as a consequence arguably wrong as it overstates the solution to the problem, and hence interferes with adapting that solution to the environment, as with the example of parallelization.
Those who would argue that garbage collection is never the right solution to heap management, in principle as well as in practice, need to put forward a theoretical basis for why heap management is a fundamentally different problem from any of the other things that compilers presently do so well.
Craig, you are of course taking my view to the extreme. It is of course a question of cost/benefit.
Extremes rarely make good arguments. But to take you up on your specific point:
"put forward a theoretical basis for why heap management is a fundamentally different problem from any of the other things that compilers presently do so well."
It somewhat beggars belief that you need to ask for this, since the distinction is patently clear.
What "compilers do so well" is take program instructions in a human readable form and convert them into instructions that a processor can execute.
The instructions involved are essentially discrete, self-contained steps. Adding two integers is adding two integers and the most efficient way for a given CPU to do that does not change depending on whether the larger application is a number cruncher or a database app.
But choosing how and when to allocate memory requires strategies that by definition WILL vary considerably not only between different types of applications but which may vary even in two different applications of the same basic type.
Heap management, in all but it’s simplest terms (Allocate/Deallocate) is simply not something that a compiler can do better than, or perhaps even as well as, a developer, who has the necessary wider understanding of the complete system architecture.
My post argues that although garbage collection isn’t appropriate in all cases (a point I restated several times, although it apparently didn’t sink in), it does have certain advantages. To call that nonsense (comparing it to a house without structural integrity) without even commenting on the advantages themselves *is* extreme, full stop. There is ample evidencethat Delphi programmers, present company not excluded, are less reliable memory stewards than anti-GC zealots would like us to believe.
As Delphi/Win32 programmers (or substitute C++, etc.) we train ourselves to work within the confines of manual memory release. When one becomes good at it, it’s easy to deceive oneself into thinking that the walls aren’t so very high — at least, until you step back and learn Haskell. Haskell was an eye-opening experience for me because it does nearly /everything/ different from Delphi. For example, the order of statement execution is so complex that it appears to border on non-deterministic. It really made me learn to question assumptions about how code works.
Once more, I’ll say that I’m not asserting that GC is always or even mostly the best solution. I especially don’t think it’s a good fit for Delphi for Win32, and have little interest in trying to retrofit a GC into that platform. But I’ll not pretend that both manual memory release and GC don’t both have advantages and disadvantages.
One last point — more of a PS, since it’s a tangent. Compilers don’t simply translate code into machine instructions via trivial rules. There are lots of ways to add two integers, to use your example. Read the dragon book.
Jolyon-
"Heap management, in all but it’s simplest terms (Allocate/Deallocate) is simply not something that a compiler can do better than, or perhaps even as well as, a developer, who has the necessary wider understanding of the complete system architecture."
Are you trying to be serious, or is this just a troll?
Think about what you’re saying. The programmer /always/ knows every possible detail of how memory should be used, and the compiler /always/ knows nothing at all?
Have you ever used long strings? Would you prefer if the compiler didn’t automatically refcount them for you, and you had to do all your string manipulation with PChar and GetMem?
I’m sorry, but there are plenty of cases — string manipulation as one example among many — where the compiler is /way/ smarter than the programmer about how to manage memory. We can certainly argue about how far this goes, but it’s downright absurd to claim that the compiler /never/ has a clue.
Post a Comment