Garbage Collection in The Age of Smart Pointers
A few days ago I was asked on Twitter whether research into garbage collection is paying off, considering the super-smart-pointers introduced into C++ and other language and library tricks. Let’s take a look at some of the smart pointer facilities introduced in C++11, and then tackle them from a garbage collection perspective.
C++11 features three standard smart pointer classes:
- unique_ptr<T> wraps a pointer to a value and guarantees that there is only one pointer at a time to that value. You cannot copy unique_ptr<T> instances around (only move them), and when the unique_ptr<T> instance is destroyed, the underlying pointer is deleted.
- shared_ptr<T> wraps a pointer to a value and a reference count, allowing multiple locations in the program to point to the same value at the same time. You can copy shared_ptr<T> instances around, and when the last shared_ptr<T> instance with a given pointer is destroyed, the underlying pointer is deleted.
- weak_ptr<T> wraps a pointer to a value and can be converted to a shared_ptr<T>, temporarily (using the lock() method). However, while you have only a weak_ptr<T> instance, the underlying pointer can be deleted (because there are no “strong” pointers to it), and then your attempt to convert it to a shared_ptr<T> will fail gracefully. (This is very similar to the implementation of short weak references in garbage collected languages, such as .NET’s WeakReference class.)
From a garbage collection perspective, unique_ptrs are not that interesting because they wrap access to a uniquely owned pointer, which is not available to anyone else. Shared_ptrs and weak_ptrs, however, are more interesting, because they provide actual semantics for shared ownership of resources and their eventual reclamation. Moreover, the fundamental problem of reference counting GC, namely that of cycles (e.g. see Python GC treatment of this issue), is addressed by providing a fairly convenient weak pointer concept.
What are the benefits of using automatic garbage collection when you have shared pointers and weak pointers, and don’t have to call “delete” in your C++ programs anymore?
Reference counting is costly. Every copy of a reference (shared_ptr, in C++’s case), involves updating a reference count, and that update requires multiprocessor synchronization. Even worse, if an object is shared across multiple threads, the reference count updates may invalidate cache lines on multiple processors. In the C++ shared_ptr, only the control block will be invalidated, but in “legacy” reference counting environments, where the reference count is embedded in the object, the object itself may be invalidated, which has an even higher performance cost.
Using weak_ptr is counter-intuitive and hard. As a programmer, I don’t want to be forced to analyze and break every reference cycle in my application. There are the obvious textbook examples with managers referencing their direct reports referencing back their managers, which can be broken with weak_ptrs; however, imagine a cycle that is not formed deterministically and that contains dozens of objects – even figuring out that you have a cycle is an impossible task.
Using smart pointers requires extraordinary consistency. C++ developers might be able to go on a “delete-elimination” crusade and convert their entire application to use smart pointers. I just don’t see this library feature ever being as convenient as standard C# references.
Reclaiming many objects at once might be more efficient than reclaiming each individual object. You may have heard horror stories about multi-second delays introduced by garbage collections (missiles diverted from their path, anyone?), but most garbage collections have sub-millisecond times, and most applications, even on fully-loaded servers, don’t suffer by default from garbage collection latencies. In fact, when you think about it, garbage collection costs (in the small object heap) are roughly linear in the number of live objects, so if most of your heap is garbage, collecting it is a fairly rapid task. On the other hand, if you reclaimed each object individually, you’d pay the same price for each piece of that garbage, instead of reclaiming it in one fell swoop.
Now, if you consider some of the “futuristic” advances in garbage collection, you might be even more inclined to prefer it over smart pointers. Take a look at Azul’s Zing pauseless garbage collection, which runs concurrently with the application and introduces only very rare and short pause times, even for huge (~100GB) heaps. If this is the future of garbage collection, we won’t need smart pointers.
I am posting short updates and links on Twitter as well as on this blog. You can follow me: @goldshtn