86

These days, so many languages are garbage collected. It is even available for C++ by third parties. But C++ has RAII and smart pointers. So what's the point of using garbage collection? Is it doing something extra?

And in other languages like C#, if all the references are treated as smart pointers(keeping RAII aside), by specification and by implementation, will there still be any need of garbage collectors? If no, then why is this not so?

Walter
  • 16,158
  • 8
  • 58
  • 95
Gulshan
  • 9,402
  • 10
  • 58
  • 89
  • 1
    One thing I understood after asking this question- __Smart pointers needs RAII to manage the automatic deallocation__. – Gulshan Dec 27 '10 at 12:45
  • 9
    Smart pointers mean using RAII for GC ;) – Dario Dec 27 '10 at 13:06
  • Heh, c# should have an option for handling all "garbage collection" with RAII. Circular references can be detected on application shutdown, all we need is see which allocations are still in memory after Program.cs-class has been deallocated. Then circular references can be replaced with some kind of week references. – AareP Feb 24 '11 at 06:15
  • 5
    An answer- http://blogs.microsoft.co.il/blogs/sasha/archive/2012/01/12/garbage-collection-in-the-age-of-smart-pointers.aspx – Gulshan Jan 27 '12 at 03:42

11 Answers11

82

So, what's the point of using garbage collection?

I'm assuming you mean reference counted smart pointers and I'll note that they are a (rudimentary) form of garbage collection so I'll answer the question "what are the advantages of other forms of garbage collection over reference counted smart pointers" instead.

  • Accuracy. Reference counting alone leaks cycles so reference counted smart pointers will leak memory in general unless other techniques are added to catch cycles. Once those techniques are added, reference counting's benefit of simplicity has vanished. Also, note that scope-based reference counting and tracing GCs collect values at different times, sometimes reference counting collects earlier and sometimes tracing GCs collect earlier.

  • Throughput. Smart pointers are one of the least efficient forms of garbage collection, particularly in the context of multi-threaded applications when reference counts are bumped atomically. There are advanced reference counting techniques designed to alleviate this but tracing GCs are still the algorithm of choice in production environments.

  • Latency. Typical smart pointer implementations allow destructors to avalanche, resulting in unbounded pause times. Other forms of garbage collection are much more incremental and can even be real time, e.g. Baker's treadmill.

J D
  • 2,332
  • 24
  • 17
  • 31
    Can't believe this answer ended up top answer. It shows a total lack of understanding of C++ smart pointers and makes claims that are so much out of sync with reality that its simply ridiculous. First, the smart pointer that in a well designed piece of C++ code will be most dominant is the unique pointer, not the shares pointer.http://en.cppreference.com/w/cpp/memory/unique_ptr And secondly, I can't believe you actually claiming 'performance' advantages and real-time advantages of non deterministic garbage collection over smart pointers. – user1703394 Jan 26 '16 at 09:46
  • 4
    @user1703394 It appears that the answerer had shared pointers in mind (rightly or wrongly, I'm not quite sure what the OPs suggesting), which are less performant than non deterministic garbage collection. – Nathan Cooper Jan 26 '16 at 14:40
  • 4
    @user1703394: "First, the smart pointer that in a well designed piece of C++ code will be most dominant is the unique pointer, not the shares pointer". I explained why I am addressing `shared_ptr` in the first sentence of my answer. – J D Jan 26 '16 at 20:25
  • 13
    @user1703394: "I can't believe you actually claiming 'performance' advantages and real-time advantages of non deterministic garbage collection over smart pointers". The fact that garbage collection is much faster than scope-based reference counting is well known, has been widely documented in GC research papers over the past 55 years, is explained in every introductory text book on the subject (e.g. the GC Handbook by Jones et al.) and I have personally measured and documented this fact: http://flyingfrogblog.blogspot.co.uk/2011/01/boosts-sharedptr-up-to-10-slower-than.html – J D Jan 26 '16 at 20:27
  • 5
    @user1703394: "non deterministic garbage collection". Experience has taught me that people who claim GC is "non-deterministic" generally don't know what that means (OCaml's GC is non-deterministic) and usually believe that reference counting is "deterministic". In fact thread-safe `shared_ptr` is also non-deterministic. Specifically, threads race to decrement to zero and the loser of the race condition is burdened with cleanup. I've seen this so much that I wrote two articles about memory management myths: http://flyingfrogblog.blogspot.co.uk/2013/10/thread-safe-reference-counting-is-not.html – J D Jan 26 '16 at 20:36
  • 2
    and here's the one about promptness (before you start claiming that reference counting collects at the earliest opportunity): http://flyingfrogblog.blogspot.co.uk/2013/10/memory-management-myths-promptness.html – J D Jan 26 '16 at 20:37
  • 8
    These are all straw man arguments and only valid if you either completely ignore the actual question or ignore actual usage patterns of the different types of smart pointers. The question was about smart pointers. Yes shared_ptr is a smart pointer and yes shared_ptr is the most expensive of smart pointers, but no, there is no actual argument for their pervasive use anywhere close to making any performance argument close to relevant. Seriously, this answer should be moved to a question on reference counting. I'ts a poor reference counting answer to a good smart pointer question. – user1703394 Feb 09 '16 at 15:02
  • 2
    @user1703394: "ignore actual usage patterns of the different types of smart pointers". I have observed these phenomena many times on real C++ code bases over the past 20 years. You don't even need to use `shared_ptr` to see unbounded pauses due to destructors avalanching. – J D Feb 09 '16 at 15:24
  • 3
    @user1703394: "this answer should be moved to a question on reference counting. I'ts a poor reference counting answer to a good smart pointer question". You seem to be having a purely emotional response to these facts. – J D Feb 09 '16 at 15:25
  • 2
    Again, how of that unbounded pauses due to destructors avalanching were actually related to the usage of the broader concept of smart pointers? And what exactly is 'emotional' about pointing out that you are not actually answering the original question by 'assuming' the question only refers to pervasive use of the least pervasively used type of smart pointer. – user1703394 Feb 09 '16 at 16:20
  • @user1703394: "broader concept of smart pointers". Smart pointers are not a broader concept. – J D Feb 09 '16 at 22:48
  • @user1703394: "the least pervasively used type of smart pointer". You are implying that `weak_ptr` is more common than `shared_ptr`. – J D Feb 09 '16 at 22:48
  • 4
    "Smart pointers are not a broader concept" , Seriously? You have no idea how much that statement undermines all possibly valid arguments you made so far. Maybe have a look at Rust ownership and move semantics: http://koerbitz.me/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html It's easy for people with historic experience with C++ to miss out on the fact that C++11/C++14 with its memory model, improved smart pointers and move semantics is a whole different beast than their predecessors. Have a look at how Rust does it, it is cleaner than C++ and provide fresh perspective. – user1703394 Feb 10 '16 at 08:29
  • 7
    @user1703394: "You have no idea". These ad-hominem attacks are pointless. If there is a reason why you think anything you have referred to is even relevant to this discussion please state it specifically. I agree that Rust is cleaner and safer than C++ and provides a new perspective but that has nothing whatsoever to do with this discussion. Unbounded pauses due to destructors avalanching will be just as prevalent in Rust as they are in C++. Move semantics address a completely unrelated problem. Here is the infinite loop in Rust: https://doc.rust-lang.org/stable/nomicon/destructors.html – J D Feb 10 '16 at 12:38
  • 1
    Let me try it one last time before I give up. 1) The OP asked about smart pointers. 2) There are multiple types of smart pointers, not just reference counted smart pointers. 3) The "don't share if you can move" mantra is extremely useful for robust responsive system design and eliminates quite a few reference counted smart pointers in favor of smart pointers with less overhead. 4) Every C++ training program today teaches shared_ptr is a last resort smart pointer type. 5) Your 'reference counted' smart pointer arguments imply a pervasive use of what should be considered a 'last resort'. – user1703394 Feb 11 '16 at 09:51
  • Unbounded pauses due to destructors is an unfortunate property of RAII being used for non-memory resources. Sometimes appropriate but not for real-time settings.There are coding guidelines that deal with that. In any case, RAII for non-memory resources is a whole different subject thus your argument not specific to any added benefits of GC over only using smart pointers. – user1703394 Feb 11 '16 at 09:53
  • 10
    @user1703394: "Unbounded pauses due to destructors is an unfortunate property of RAII being used for non-memory resources". No, this has nothing whatsoever to do with non-memory resources. – J D Feb 11 '16 at 10:28
  • "Once those techniques are added, reference counting's benefit of simplicity has vanished." Plain wrong. Using of `std::weak_ptr` does not obviate the benefit of determinism. The latency problem here is red herring - it is eventually the programmers to control whether it is bounded at least in static languages. Tracing collectors usually provides no options to be controlled with less amortized cost, which is a nest loss in such cases. – FrankHB May 15 '18 at 07:51
  • Practically, if throughput has to be ultimately emphasized, the solution of resource management for best performance would be some form of explicit management (which is often required to be replaced by RC for simplicity and maintainability)+pooling. The latter can be considered a form of _local GC_ (actually not quite, because it is still deterministic about deallocation) for the purpose of performance, but it is not aiming to replace the position of RC. Using GC by default only wins in one way: avoiding reinventing the wheels of an optimized complicated _memory_ management system like that. – FrankHB May 15 '18 at 07:57
  • One more point I'd say here is the misconception on determinism. Note that even in C++ there is no space complexity requirements/guarantees in the core language rules, so normatively, the effect of space cost only lives in implementations. Surely it is still heavily cared about by users in reality, but this should not come at first in languages allowing _programmable side effects on finalization_. For languages without such ability, there is few need to compare, as you don't compare machine languages to them. Sadly, you have separated wrong concerns at the very early stage. – FrankHB May 15 '18 at 08:30
  • To name a few, the effects mentioned above include destruction (like in C++) and (postconditional) contract checking in (like in Racket). Such effects are utilized in a way of abstraction to ensure some important invariance (e.g. property of no conventional memory leaking under ALGOL-style block allocation defined in [Clinger98] paper, and further, also suitable for non-memory resources), which make the program to be easier reasoned (e.g. to inspect the ownership between objects), at least by humans. Erasure of such information is the exact "optimization" leads to using of GC . – FrankHB May 15 '18 at 08:31
  • 1
    @FrankHB: "Plain wrong. Using of std::weak_ptr does not obviate the benefit of determinism". weak_ptr isn't a technique to catch cycles. – J D May 15 '18 at 15:23
  • 1
    @FrankHB: "The latency problem here is red herring - it is eventually the programmers to control whether it is bounded at least in static languages. Tracing collectors usually provides no options to be controlled with less amortized cost, which is a nest loss in such cases". The JVM and CLR are obvious counter examples, e.g. Rapid Addition's real-time FIX engine relies upon this. I have used these techniques myself in production. – J D May 15 '18 at 15:26
  • 1
    @FrankHB: "if throughput has to be ultimately emphasized, the solution of resource management for best performance would be some form of explicit management (which is often required to be replaced by RC for simplicity and maintainability)+pooling". RC has the worst throughput performance of these techniques. – J D May 15 '18 at 15:33
  • Well, my bad. The quote should also include "Reference counting alone leaks cycles so reference counted smart pointers will leak memory in general unless other techniques are added to catch cycles." More specifically, `weak_ptr` is a technique to _avoid_ improper cycles. Keep acyclic or die, and there will be no leaks. I'd agree RC is not sufficient to debug a program already messed up with unexpected cycles so something playing the role of tracing GC should be there, but it can be out of the user program (by valgrind, etc). – FrankHB May 16 '18 at 01:57
  • I was not criticizing the bad performance of latency caused by tracing GC. Tracing collectors can provide smooth end-user experience in several cases by default, but they usually do not provide detailed interface to control how local resources live. In most times the programmer can do no more than a call the explicit "collect" method on collectors _in the object languages_ (and it could even be ignored...). Tuning an existed collector is also quite difficult (if not impossible) by coding the strategy together. – FrankHB May 16 '18 at 02:16
  • RC is not _always_ worse in throughput. I don't believe you can find formal proofs as it depends on too many things vary in runtime environments. There are rooms to experiment. Note to _implement_ RC is far simpler than to implement tracing techniques correctly, so it deserves more chances to be optimized in practice when such a specific solution is really needed. For instance, in single-threaded code I can replace `std::shared_ptr` by my homebrew smart pointer with almost exactly compatible API quite trivially and expect it faster, but what about replacing a concurrent GC? – FrankHB May 16 '18 at 02:28
  • @FrankHB: "I'd agree RC is not sufficient to debug a program already messed up with unexpected cycles so something playing the role of tracing GC should be there". There are alternatives like Trial Deletion. – J D May 16 '18 at 09:11
  • 1
    @FrankHB: "Note to implement RC is far simpler than to implement tracing techniques correctly". Not really. The simple algorithms are described succinctly in https://www.cs.virginia.edu/~cs415/reading/bacon-garbage.pdf and more sophisticated forms are complicated in both cases (e.g. deferred decrements with RC via message passing between threads). – J D May 16 '18 at 09:13
  • 2
    @FrankHB: "so it deserves more chances to be optimized in practice when such a specific solution is really needed". Almost 60 years of GC research has shown that even the most optimized RC solution still has lower throughput than tracing GC. The problem is that incrementing and decrementing reference counts when references are manipulated incurs a lot of cache misses. – J D May 16 '18 at 09:15
  • 2
    @FrankHB: "in single-threaded code I can replace std::shared_ptr by my homebrew smart pointer with almost exactly compatible API quite trivially and expect it faster, but what about replacing a concurrent GC". True but your replacement inherits the same limitations from C++. You cannot walk the stack efficiently in C++ to find global roots. You cannot determine what each object references so you cannot trace the heap easily using C++. Hence with C++ you are limited to the likes of `shared_ptr`. – J D May 16 '18 at 09:25
  • Compare with something like HLVM where the thread-safe tracing GC is ~100 lines of code and easily changed: http://www.ffconsultancy.com/ocaml/hlvm/ – J D May 16 '18 at 09:25
74

Since no one has looked at it from this angle, I'll rephrase your question: why put something into the language if you can do it in a library? Ignoring specific implementation and syntactic details, GC/smart pointers is basically a special case of that question. Why define a garbage collector in the language itself if you can implement it in a library?

There are a couple of answers to that question. The most important first:

  1. You ensure that all code can use it to interoperate. This is, I think, the big reason why code reuse and code sharing didn't really take off until Java/C#/Python/Ruby. Libraries need to communicate, and the only reliable shared language they have is what's in the language spec itself (and, to a degree, its standard library). If you've ever tried to reuse libraries in C++, you've likely experienced the horrendous pain that no standard memory semantics causes. I want to pass a struct to some lib. Do I pass a reference? Pointer? scoped_ptr? smart_ptr? Am I passing ownership, or not? Is there a way to indicate that? What if the lib needs to allocate? Do I have to give it an allocator? By not making memory management part of the language, C++ forces each pair of libraries to have to negotiate their own specific strategy here, and it's really hard to get them all to agree. GC makes that a complete non-issue.

  2. You can design the syntax around it. Because C++ doesn't encapsulate memory-management itself, it has to provide a range of syntactic hooks to let user-level code express all of the details. You have pointers, references, const, dereferencing operators, indirection operators, address-of, etc. If you roll memory management into the language itself, the syntax can be designed around that. All of those operators disappear and the language gets cleaner and simpler.

  3. You get a high return on investment. The value any given piece of code generates is multiplied by the number of people using it. This means that the more users you have, the more you can afford to spend on a piece of software. When you move a feature into the language, all users of the language will be using it. This means you can allocate more effort to it than you could to a library only used by a subset of those users. This is why languages like Java and C# have absolutely first-rate VMs and fantastically high-quality garbage collectors: the cost of developing them is amortized across millions of users.

munificent
  • 2,029
  • 13
  • 11
  • Fantastic answer! If only I could upvote more than once... – Dean Harding Dec 28 '10 at 01:10
  • 10
    It's worth noting that garbage collection is not actually implemented in the C# language itself, but [in the .NET Framework](http://msdn.microsoft.com/en-us/library/0xy59wtx.aspx), specifically the Common Language Runtime (CLR). – Robert Harvey Jan 04 '11 at 02:37
  • 9
    @RobertHarvey: It's not *implemented* by the language, but it wouldn't work without the *cooperation* of the language. For example, the compiler must include information specifying, at every point in the code, the location of every register or stack-frame offset that holds a reference to an unpinned object. That is an **absolute no-exceptions-whatsoever invariant**, which could not be upheld without language support. – supercat Dec 24 '13 at 20:14
  • 1
    A major advantage of having GC support the language and required framework is that it ensures that no references will ever exist to memory that might be allocated for some other purpose. If one calls `Dispose` on an object which encapsulates a bitmap, any reference to that object will be a reference to a disposed bitmap object. If the object was deleted prematurely while other code still expects to use it, the bitmap class can ensure that the other code will fail in *predictable* fashion. By contrast, using a reference to freed memory is Undefined Behavior. – supercat Dec 24 '13 at 20:18
38

Garbage collection basically just means that your allocated objects are automatically released at some point after they're not reachable any more.

More accurately, they're released when they become unreachable for the program, as circularly referenced objects would never get released otherwise.

Smart pointers just refer to any structure that behaves like an ordinary pointer but has some extra functionality attached. These include but are not limited to deallocation, but also copy-on-write, bound checks, ...

Now, as you have stated, smart pointers can be used to implement a form of garbage collection.

But the train of thought goes the following way:

  1. Garbage collection is a cool thing to have, as it's convenient and I have to take care of fewer things
  2. Therefore: I want garbage collection in my language
  3. Now, how can get GC into my language?

Of course, you can design it like this from start. C# was designed to be garbage collected, so just new your object and it'll be released when the references fall out of scope. How this is done is up to the compiler.

But in C++, there was no garbage collection intended. If we allocate some pointer int* p = new int; and it falls out of scope, p itself is removed from the stack, but nobody takes care of the allocated memory.

Now the only thing you have from start are deterministic destructors. When an object leaves the scope it has been created in, its destructor is called. In combination with templates and operator overloading, you can design a wrapper object that behaves like a pointer, but uses destructor functionality to clean up resources attached to it (RAII). You call this one a smart pointer.

This is all highly C++ specific: Operator overloading, templates, destructors, ... In this particular language situation, you have developed smart pointers to provide you with the GC you want.

But if you design a language with GC from start, this is merely an implementation detail. You just say object will be cleaned up and the compiler will do this for you.

Smart pointers like in C++ wouldn't probably be even possible in languages like C#'s, which have no deterministic destruction at all (C# works around this by providing syntactic sugar for calling a .Dispose() on certain objects). Unreferenced resources will finally be reclaimed by the GC, but it undefined when exactly this will happen.

And this, in turn, can allow the GC to do its work more efficient. Being built in deeper into the language than smart pointers, which are set on top of it, the .NET GC can e.g. delay memory operations and perform them in blocks to make them cheaper or even move memory around for increasing efficiency based on how often objects are accessed.

Deduplicator
  • 8,591
  • 5
  • 31
  • 50
Dario
  • 1,618
  • 10
  • 13
  • C# does have a form of deterministic destruction via `IDisposable` and `using`. But it requires a little bit of programmer effort, which is why it's usually only used for very scarce resources such as database connection handles. – JSBձոգչ Dec 27 '10 at 16:14
  • 7
    @JSBangs: Exactly. Just as C++ builds smart pointers around RAII to get GC, C# goes the other way and builds "smart disposers" around the GC to get RAII ;) In fact, it's a shame that RAII is so difficult in C# as it's great for exception-safe resource handling. F# for example tries a simpler `IDisposable` syntax by just replacing conventional `let ident = value` by `use ident = value` ... – Dario Dec 27 '10 at 16:24
  • @Dario: "C# goes the other way and builds 'smart disposers' around the GC to get RAII". RAII in C# with `using` has nothing to do with garbage collection at all, it just calls a function when a variable falls out of scope just like destructors in C++. – J D Dec 27 '10 at 17:53
  • @Dario: "Now the only thing you have from start are deterministic destructors. When an object leaves the scope it has been created in, its destructor is called". That is only true for thread-unsafe reference counted smart pointers. If you want thread safety then you increment and decrement the reference counts atomically and two threads can race to perform the final decrement, culminating in non-determinism. – J D Dec 27 '10 at 17:58
  • 1
    @Jon Harrop: Please what? The statement you cite is about plain C++ destructors, without any reference counting/smart pointers/garbage collection involved. – Dario Dec 27 '10 at 19:45
  • @Jon Harrop: As to `using`, you're right ... – Dario Dec 27 '10 at 19:50
  • @Dario: Sorry, I misread your statement about the determinism of destructors as being about reference counted smart pointers. – J D Dec 27 '10 at 20:21
  • 2
    "Garbage collection basically just means that your allocated objects are automatically released when they're not being referenced any more. More accurately, they're released when they become unreachable for the program, as circularly referenced objects would never get released otherwise." ... More accurate would be to say they are automatically released *at some point after*, not *when*. Note that *when* implies that the reclamation happens immediately, when in fact reclamation often happens way later. – Todd Lehman Jun 08 '15 at 20:33
7

There are two big differences, to my mind, between garbage collection and smart pointers as used for memory management:

  1. Smart pointers can't collect cyclic garbage; garbage collection can
  2. Smart pointers do all the work at the moments of referencing, dereferencing, and deallocation, on the application thread; garbage collection need not

The former means that GC will collect garbage that smart pointers won't; if you're using smart pointers, you have to avoid creating this kind of garbage, or be prepared to deal with it manually.

The latter means that no matter how smart smart pointers are, their operation will slow down the working threads in your program. Garbage collection can defer work, and move it to other threads; that lets it be more efficient overall (indeed, the runtime cost of a modern GC is less than a normal malloc/free system, even without the extra overhead of smart pointers), and do what work it still needs to do without getting in the way of the application threads.

Now, note that smart pointers, being programmatic constructs, can be used to do all sorts of other interesting things - see Dario's answer - which are completely outside the scope of garbage collection. If you want to do those, you will need smart pointers.

However, for the purposes of memory management, i don't see any prospect of smart pointers replacing garbage collection. They simply aren't as good at it.

Tom Anderson
  • 3,022
  • 19
  • 24
  • I don't think these are necessary, i.e. inherent drawbacks of smart pointers, just common "faults" of existing implementations. You could very well delegate actual, indeterministic deallocation at the destructor call of the pointer, as well as look up cyclic references. – Dario Dec 27 '10 at 12:57
  • 1
    You’re confusing concept and implementation, I think. While most smart pointers use reference counting, not all do. As such, *some* smart pointers *may* be able to deal with cyclic garbage. Furthermore, “they aren’t as good” is a blanket statement which is simply false, as smart pointers have considerable advantages over GCs. – Konrad Rudolph Dec 27 '10 at 12:57
  • @Konrad: perhaps. My understanding of the term 'smart pointer' is that it refers to a very specific idiom in C++ which uses a reference-counting wrapper around a pointer. On that basis, smart pointers essentially *are* an implementation. What other kinds of smart pointer are there that i should know about? As for the blanket statement - i am not aware of any considerable advantages of smart pointers over GC, so i stand by it. I would of course be interested to hear of any such advantages. – Tom Anderson Dec 27 '10 at 13:21
  • @Tom Anderson: Various kinds of GC (reference-counting, plain `delete`, cycle detection), Bounds checking, copy-on-write, ... http://en.wikipedia.org/wiki/Smart_pointer. Basically, even iterators are smart pointers as they enrich the normal functionality of pointers while providing the same syntax. – Dario Dec 27 '10 at 13:25
  • 9
    @Tom: have a look at Dario’s answer for details about smart pointers. As for advantages of smart pointers – deterministic deallocation can be an enormous advantage when used to control resources (not only memory). In fact, this has proven *so* important that Microsoft has introduced the `using` block in subsequent versions of C#. Furthermore, the nondeterministic behaviour of GCs can be forbidding in real-time systems (which is why GCs aren’t used there). Also, let’s not forget that GCs are *so* complex to get right that most actually leak memory and are quite inefficient (e.g. Boehm …). – Konrad Rudolph Dec 27 '10 at 13:26
  • You're absolutely right about the use of RAII via smart pointers - i think about it as a separate thing, but the implementations are certainly entwined. I'll edit my answer to mention that. There is still no good way to do anything similar in Java, and it's a pain everyone feels. – Tom Anderson Dec 27 '10 at 13:47
  • 6
    The nondeterminism of GCs is, i think, a bit of a red herring - there are GC systems which are suitable for realtime use (like IBM's Recycler), even though the ones you see in desktop and server VMs aren't. Plus, using smart pointers means using malloc/free, and conventional implementations of malloc are nondeterministic due to the need to search the free list. Moving GC systems have *more* deterministic allocation times than malloc/free systems, although of course less deterministic deallocation times. – Tom Anderson Dec 27 '10 at 13:49
  • 3
    As for complexity: yes, GCs are complex, but i'm not aware that "most actually leak memory and are quite inefficient", and would be interested to see some evidence otherwise. Boehm is not evidence, because it's a very primitive implementation, and it's built to serve a language, C, where accurate GC is fundamentally impossible due to the lack of type safety. It's a brave effort, and that it works at all is very impressive, but you can't take it as an exemplar of GC. – Tom Anderson Dec 27 '10 at 13:50
  • I'm still interested to read about a smart pointer that does something cleverer than refcounting - cycle detection in particular - in a practical way. – Tom Anderson Dec 27 '10 at 13:59
  • @Konrad: "let’s not forget that GCs are so complex to get right that most actually leak memory and are quite inefficient". Bullshit. – J D Dec 27 '10 at 14:18
  • 8
    @Jon: decidedly *not* bullshit. https://bugzilla.novell.com/show_bug.cgi?id=621899 or, more generally: http://flyingfrogblog.blogspot.com/2009/01/mono-22-still-leaks-memory.html This is well-known and a property of all conservative GCs. – Konrad Rudolph Dec 27 '10 at 14:22
  • @Tom: the nondeterminism I was referring to is indeed deallocation nondeterminism. The problem is not so much *how long* allocation/deallocation takes (this is a fight that GCs win hands down – although you can use a pool allocator in non-GCed languages to simulate this) but *when* it happens, and this may indeed be of importance (e.g. when it’s important that this activity doesn’t interfere with other processes that are required to be reactive). – Konrad Rudolph Dec 27 '10 at 14:25
  • @Konrad: and as i said, there are realtime collectors that address that - IBM's Metronome collector is in production, and guarantees GC pauses no greater than 500 microseconds. That might not be enough for some applications, of course. As for documented problems with Boehm - those are not inevitable problems with GC, or a result of its complexity; they are direct consequences of the fact that you just can't do accurate GC in C. I don't know why Mono is using Boehm - seems pretty nutty to me. – Tom Anderson Dec 27 '10 at 16:16
  • 4
    "The runtime cost of a modern GC is less than a normal malloc/free system." Red herring here. This is only because traditional malloc is a horribly inefficient algorithm. Modern allocators that use multiple buckets for different block sizes are much faster to allocate, far less prone to heap fragmentation, and still give you fast deallocation. – Mason Wheeler Dec 27 '10 at 17:25
  • 1
    @Mason Wheeler: Quick test using a custom pool allocator in C++ vs OCaml: C++ is still 70% slower than OCaml. – J D Dec 28 '10 at 14:23
  • 1
    @Tom: "GCs are complex". My GC in HLVM is ~100 lines of code. GCs can be simple... – J D Dec 29 '10 at 23:04
  • @Konrad: "This is well-known and a property of all conservative GCs". I agree but your generalization of that to all GCs was bullshit. – J D Dec 29 '10 at 23:07
  • Boehm GC isn't that bad of an implementation. But most people use it like a brainless throw in conversative collector while it is very adaptable and if you are a language developer you really should care about a much tighter integration. – Lothar Nov 06 '13 at 17:34
  • 1
    @KonradRudolph: Code shouldn't care about when memory is reclaimed by the GC. GC is not an appropriate way of managing resources other than fully-fungible memory, and I consider it unfortunate that resource management in popular GC frameworks seems like an afterthought. – supercat Dec 24 '13 at 20:21
  • 1
    “GC is not an appropriate way of managing resources” – I agree. But deterministic object life-time *is* an appropriate way of managing non-memory resources (see C++ & RAII). Like you said, it’s unfortunate that this way of handling resources is lost with a GC. – Konrad Rudolph Dec 24 '13 at 21:47
  • @KonradRudolph: There's no reason it shouldn't be able to coexist beautifully with GC. In a GC-based system, using a reference to an object which has been abandoned and replaced with a new one won't trap but won't have the desired effect either. In a RAII-based system, using a reference to a destroyed object will cause Undefined Behavior. Combining the two approaches would make it possible to have both situations deterministically trap. – supercat Jan 13 '15 at 17:16
  • @supercat Do you have an example of such a system? Note, this may be very obvious, and I’m not contesting your claim at all. In fact, my comments above are probably quite specific to GCs such as those found in C#/.NET and Java. – Konrad Rudolph Jan 13 '15 at 17:30
  • 1
    @KonradRudolph: C++/CLI tries somewhat to combine the approaches, though .NET isn't really set up for it. For example, if a type in C++ is declared as "owning" a field of an `IDisposable`, an auto-generated `Dispose` method will call `Dispose` on it; further, if a constructor throws the compiler will automatically generate a `Dispose` on the partially-constructed object. Unfortunately, if another language like C# is used to code a class derived from one coded in C++/CLI, and the derived class constructor throws, the base-class `Dispose` won't get called. – supercat Jan 13 '15 at 18:01
  • @KonradRudolph: Still, there wouldn't have been any technical obstacle to having `Object` contain a protected virtual `LifetimeNotification` method with parameters indicating what phase of its lifetime it was entering. The .NET framework could then, among other things, call that method whenever a constructor either completed normally or via an exception (with parameters indicating which). Such a design would assist with many things, including resolving problems with having a base class sign up for and start receiving events before derived-class construction was complete. – supercat Jan 13 '15 at 18:04
  • @KonradRudolph: Each class layer could be expected to use its "main" constructor to put itself into a state where it was ready to receive virtual calls from the base, but refrain from doing anything that would trigger any virtual calls until the `LifetimeNotification` method was invoked. Adding such a thing to .NET at this point probably wouldn't be feasible, but there's no reason the next popular OO framework couldn't have one. – supercat Jan 13 '15 at 18:08
  • @supercat: "if another language like C# is used to code a class derived from one coded in C++/CLI, and the derived class constructor throws, the base-class Dispose won't get called". Firstly, these are problems with OOP not GC. Secondly, they can be solved by not leaking destructors everywhere, e.g. look at the design and use of IEnumerable on .NET and `seq` in F#. – J D May 16 '18 at 09:42
  • @JonHarrop: If a derived-class constructor throws after the base is constructed, there is no remotely-clean mechanism by which the base class can ever determine that has happened. If the base class was simply a wrapper and any subscriptions that it established were to the underlying object or unrelated wrappers, then the base class object would be eligible for `Finalize` if a derived-class constructor throws, but that's not really a good solution. – supercat May 16 '18 at 14:39
5

The term garbage collection implies there is any garbage to collect. In C++ smart pointers come in multiple flavors, most importantly the unique_ptr. The unique_ptr is basically a single ownership and scoping construct. In a well designed piece of code most heap allocated stuff would normally reside behind unique_ptr smart pointers and ownership of those resources will be well defined at all times. There is hardly any overhead in unique_ptr and unique_ptr takes away most of the manual memory management problems that traditionally drove people to managed languages. Now that more cores running concurrently are becoming more common good, the design principles that drive code to use unique and well defined ownership at any point in time becomes more important for performance. The use of the actor model of computation allows for the construction of programs with a minimum amount of shared state between threads, and unique ownership plays a major role in making high performance systems make efficient use of many cores without the overhead of shared-between-threads data and the implied mutex requirements.

Even in a well designed program, especially in multi threaded environments, not everything can be expressed without shared data structures, and for those data structures that truly require, threads need to communicate. RAII in c++ works pretty well for lifetime concerns in a single threaded setup, in a multi threaded setup the lifetime of objects may not be completely stack hierarchically defined. For these situations, the use of shared_ptr offers a big part of the solution. You create shared ownership of a resource and this in C++ is the only place we see garbage, but at such small quantities that a proper designed c++ program should be considered more to implement 'litter' collection with shared-ptr's than full fledged garbage collection as implemented in other languages. C++ simply does not have that much 'garbage' to collect.

As stated by others, reference counted smart pointers are one form of garbage collection, and a for that has one major issue. The example that is used mostly as drawback of reference counted forms of garbage collection is the problem with the creation of orphaned data structures connected with smart pointers to each other that create object clusters that keep each other from being collected. While in a program designed according to the actor-model of computation, the data structures not usually allow for such noncollectable clusters to arise in C++, when you use the broad shared data approach to multi threaded programming, as is used predominantly in a large part of the industry, these orphaned clusters can quickly become a reality.

So to sum it all up, if by shared pointer usage you mean the wide use of unique_ptr combined with the actor model of computation approach for multi threaded programming and the limited use of shared_ptr, than other forms of garbage collection don't buy you any added benefits. If however a shared everything approach would have you end up with shared_ptr all over the place, than you should consider either switching concurrency models or switching to a managed language that is more geared towards the wider sharing of ownership and concurrent access to data structures.

user1703394
  • 234
  • 2
  • 4
4

Most smart pointers are implemented using reference counting. That is, each smart pointer that refers to an object increments the objects reference count. When that count goes to zero, the object is released.

The problem there is if you have circular references. That is, A has a reference to B, B has a reference to C and C has a reference to A. If you're using smart pointers, then in order to free the memory associated with A, B & C you need to manually get in there an "break" the circular reference (e.g. using weak_ptr in C++).

Garbage collection (typically) works quite differently. Most garbage collectors these days use a reachability test. That is, it looks at all of the references on the stack and the ones that are globally accessible and then traces every object that those references refer to, and objects they refer to, etc. Everything else is garbage.

In that way, circular references don't matter any more - as long as neither A, B and C are reachable, the memory can be reclaimed.

There are other advantages to "real" garbage collection. For example, memory allocation is extremely cheap: just increment the pointer to the "end" of the memory block. Deallocation has a constant amortized cost as well. But of course languages like C++ allow you to implement memory management pretty much any way you like, so you could come up with an allocation strategy that's even faster.

Of course, in C++ the amount of heap-allocated memory is typically less than a reference-heavy language like C#/.NET. But that's not really a garbage-collection vs. smart pointers issue.

In any case, the issue isn't cut-and-dry one is better than the other. They each have advantages and disadvantages.

Dean Harding
  • 19,871
  • 3
  • 51
  • 70
3

Garbage collection can be more efficient - it basically 'batches up' the overhead of the memory management and does it all at once. In general this will result in less overall CPU being expended on memory de-allocation, but it means that you'll have a big burst of de-allocation activity at some point. If the GC isn't properly designed this can become visible to the user as a 'pause' while the GC tries to de-allocate memory. Most modern GCs are very good at keeping this invisible to the user except under the most adverse conditions.

Smart pointers (or any reference counting scheme) have the advantage that they happen exactly when you'd expect from looking at the code (smart pointer goes out of scope, thing gets deleted). You get little bursts of de-allocation here and there. You overall may use more CPU time on de-allocation, but since it's spread out over all of the things happening in your program, it's less likely (baring de-allocate of some monster data structure) to become visible to your user.

If you are doing something where responsiveness matters, I would suggest that smart pointers/ref counting let you know exactly when things are happening, so you can know while coding what's likely to become visible to your users. In a GC setting you have only the most ephemeral of control over the garbage collector and simply have to try to work around the thing.

On the other hand, if overall throughput is your goal, a GC based system may be a much better choice, as it minimizes the resources needed to do memory management.

Cycles: I do not consider the problem of cycles to be a significant one. In a system where you have smart pointers, you tend toward data structures that don't have cycles, or you are simply careful about how you let go of such things. If necessary, keeper objects that know how to break the cycles in the owned objects can be used in order to automatically insure proper destruction. In some realms of programming this may be important, but for most day-to-day work, it's irrelevant.

Michael Kohne
  • 10,038
  • 1
  • 36
  • 45
  • 1
    "you'll have a big burst of de-allocation activity at some point". Baker's treadmill is an example of a beautifully incremental garbage collector. http://www.memorymanagement.org/glossary/t.html#treadmill – J D Dec 27 '10 at 18:25
2

It's about performance. Unallocating memory requires lot of administration. If the unallocation runs in the background, the performance of foreground process increases. Unfortunatelly, memory allocation can't be lazy (the objects allocated will be used at the holy next moment), but releasing objects can.

Try in C++ (w/o any GC) to allocate a big bunch of objects, print "hello", then delete them. You'll be surprised how long does it take to free objects.

Also, GNU libc provides more effective tools for unallocating memory, see obstacks. Must notice, I have no experience with obstacks, I never used them.

ern0
  • 1,035
  • 3
  • 8
  • 14
  • In principle you have a point but it should be noted that this is an issue that has a very simple solution: use a pool allocator or small object allocator to bundle deallocations. But this admittedly takes (slightly) more effort than having a GC run in background. – Konrad Rudolph Dec 27 '10 at 14:29
  • Yep, sure, GC is much more comfortable. (Especially for beginners: there're no ownership problems, there's even no delete operator.) – ern0 Dec 27 '10 at 14:46
  • 3
    @ern0: no. The whole point of (reference counting) smart pointers is that there is no ownership problem and no delete operator. – Konrad Rudolph Dec 27 '10 at 14:57
  • @Konrad: The only solves the performance problem in the context of single-threaded applications. – J D Dec 27 '10 at 15:29
  • 3
    @Jon: which, honestly, is most of the time. If you willy-nilly share object state between different threads you will have completely different problems. I’ll admit that many people program that way but this is a consequence of the bad threading abstractions that have existed until recently and it’s not a good way to do multithreading. – Konrad Rudolph Dec 27 '10 at 15:32
  • @Konrad: Asynchronous programming and Cilk-style parallel programming are obvious counter examples. – J D Dec 27 '10 at 15:57
  • @Jon: how so? I’ve never worked with Cilk but as far as I know this language places a lot of emphasis on communication. Wildly sharing object state is the *opposite* of that (i.e., it ignores communication). Aren’t objects usually either owned by a parent thread which manages its lifetime or local to one thread? Neither case requires cross-thread reference counting. – Konrad Rudolph Dec 27 '10 at 16:08
  • @Konrad: Asynchronous programming means you ask the OS to invoke your function when an asynchronous operation has completed and that usually happens in the thread pool. So asynchronous programs typically have control flow that hops between threads. Per-thread resources obviously wouldn't work in that context due to thread hopping and per-workflow pool allocators would be too heavyweight (you can have millions of workflows). Moreover, you're concurrently passing messages between threads so you would have to resolve who owns which message or deep copy everything like Erlang (which is *slow*). – J D Dec 27 '10 at 18:08
  • @Konrad: Mutating shared state is just another form of communication. The Cilk guys quantify it as *cache complexity*, the asymptotic cache miss rate on an idealized multicore, because that measures the rate at which the cache coherence hardware is communicating which is often the bottleneck in parallel programs on multicores in practice. They minimize cache complexity by mutating shared state in-place from parallel tasks that, again, execute on threads in a pool and the threads themselves are abstracted away. So you've got the same problem with your thread unsafe pool allocators. – J D Dec 27 '10 at 18:22
  • I just said that freeing an object costs lot, so it's better to do it with a background thread, which does not suspends the foreground task. Smart pointers solve ownership problem, too, of course, but some cases auto-dropping an object by a smart pointer may cause a chain reaction of dropping other objects, which may take too long, and may occuring in an unwanted moment of time (e.g. in the middle of an UI action). – ern0 Dec 27 '10 at 20:12
  • @ern0: FWIW, Mathematica suffers from the UI pauses due to avalanches of deallocation from reference counting that you describe. I also referred to this phenomenon in my own answer. – J D Dec 28 '10 at 14:09
  • 1
    Deallocation often isn't done "in the background", but rather pauses all foreground threads. Batch-mode garbage-collection is generally a performance win, despite the pausing of foreground threads, because it allows unused space to be consolidated. One could separate the processes of garbage-collection and heap compactification, but--especially in frameworks that use direct references rather than handles--they both tend to be "stop-the-world" processes, and it's often most practical to do them together. – supercat Jul 10 '12 at 19:21
  • I bet your problem is not slow deleting but slow I/O. – Zaffy Mar 12 '19 at 21:52
  • There's no I/O during the cleanup, but hundreds of megabytes of small fragments. – ern0 Mar 18 '19 at 08:50
2

It's a spectrum.

If you wan't tight bounds on performance and are prepared to put the grind in, you'll end up at assembly or c, with all the onus on you to make the right decisions and all the freedom to do that, but with it, all the freedom to mess it up:

"I'll tell you what to do, you do it. Trust me".

Garbage collection is the other end of the spectrum. You have very little control, but its taken care of for you:

"I'll tell you what I want, you make it happen".

This has a lot of advantages, mostly that you don't need to be as trustworthy when it comes to knowing exactly when a resource is no longer needed, but (despite some of the answers floating around here) is not good for performance, and the predictability of performance. (Like all things, if you are given control, and do something stupid you can have worse results. However to suggest that knowing at compile time what the conditions are for being able to free memory, can't be use as a performance win is beyond naive).

RAII, scoping, ref counting, etc are all helpers for letting you move further along that spectrum but its not all the way there. All of these things still require active use. They still let and require you to interact with memory management in a way that garbage collection does not.

drjpizzle
  • 264
  • 2
  • 6
1

Number one limitation of smart pointers is they don't always help against circular references. For example you have object A storing a smart pointer to object B and object B is storing a smart pointer to object A. If they are left together without resetting either of the pointers they won't ever be deallocated.

This happens because a smart pointer has to perform a specific action which won't be triigered in the above scenario because both objects are unreacheable to the program. Garbage collection will cope - it will properly identify that objects are not reachecable to the program and they will be collected.

sharptooth
  • 4,349
  • 1
  • 27
  • 37
0

Please remember that in the end, everything boils down to a CPU executing instructions. To my knowledge all consumer grade CPUs have instruction sets which require you to have data stored in a given place in memory and you have pointers to said data. That's all that you have at the basic level.

Everything on top of that with garbage collection, references to data which may have been moved, heap compaction, etc. etc. is doing the work within the restrictions given by the above "memory chunk with an address pointer" paradigm. Same thing with smart pointers - you STILL have to make the code run on actual hardware.