views:

2642

answers:

8

I come from a C++ background and I've been working with C# for about a year. Like many others I'm flummoxed as to why deterministic resource management is not built-in to the language. Instead of deterministic destructors we have the dispose pattern. People start to wonder whether spreading the IDisposable cancer through their code is worth the effort.

In my C++-biased brain it seems like using reference-counted smart pointers with deterministic destructors is a major step up from a garbage collector that requires you to implement IDisposable and call dispose to clean up your non-memory resources. Admittedly, I'm not very smart... so I'm asking this purely from a desire to better understand why things are the way they are.

What if C# were modified such that:

Objects are reference counted. When an object's reference count goes to zero, a resource cleanup method is called deterministically on the object, then the object is marked for garbage collection. Garbage collection occurs at some non-deterministic time in the future at which point memory is reclaimed. In this scenario you don't have to implement IDisposable or remember to call Dispose. You just implement the resource cleanup function if you have non-memory resources to release.

  • Why is that a bad idea?
  • Would that defeat the purpose of the garbage collector?
  • Would it be feasible to implement such a thing?

EDIT: From the comments so far, this is a bad idea because

  1. GC is faster without reference counting
  2. problem of dealing with cycles in the object graph

I think number one is valid, but number two is easy to deal with using weak references.

So does the speed optimization outweigh the cons that you:

  1. may not free a non-memory resource in a timely manner
  2. might free a non-memory resource too soon

If your resource cleanup mechanism is deterministic and built-in to the language you can eliminate those possibilities.

+15  A: 

The garbage collector does not require you to write a Dispose method for every class/type that you define. You only define one when you need to explicitly do something to cleanup ; when you have explicitly allocated native resources. Most of the time, the GC just reclaims memory even if you only do something like new() up an object.

The GC does reference counting - however it does it in a different way by finding which objects are 'reachable' (Ref Count > 0) every time it does a collection... it just doesn't do it in a integer counter way. . Unreachable objects are collected (Ref Count = 0). This way the runtime doesn't have to do housekeeping/updating tables everytime an object is assigned or released... should be faster.

The only major difference between C++ (deterministic) and C# (non-deterministic) is when the object would be cleaned up. You can't predict the exact moment an object would be collected in C#.

Umpteenth plug: I'd recommend reading Jeffrey Richter's standup chapter on the GC in CLR via C# in case you're really interested in how the GC works.

Gishu
+1 Great book suggestion
SnOrfus
So, eschew RAII because reference counting is slower and you don't have to deal with Dispose on every class. Seems like a poor tradeoff to me.
Skrymsli
Didn't understand your comment here... I just explained that implementing Dispose is not mandatory for every C# type/class. GC works perfectly even if none of your classes implemented Dispose.
Gishu
I'm not sure my question was fully understood. Many many classes in the framework implmeent IDisposable. You must call dispose on those classes. It would be nice if RAII were possible to help with that. Anyway, it seems that most people agree with your reasoning.
Skrymsli
Actually, you don't _have_ to call dispose on every implementer of IDisposable. If the object is well-written/has a finalizer, the resources will still get collected. IDisposable provides a means for deterministic release if you _need_ it to be deterministic. If you only grab a particular OS resource on an infrequent basis, the non-deterministic finalization may well be good enough. Think of it as extending the classic C++ "opt-in" notion to resource management.
Greg D
How can you be sure if the object is well-written if you didn't write it yourself?
Skrymsli
@Skrymsli assume it is well-written and move on. most of the time it will be. If it's not, *and* it causes a problem, it's trivial to open the code in a tool like Reflector and find out exactly what needs to be done with regards to disposal.
Rex M
@Skrymsli: Rex M is right. And it's safe to assume that everything in the framework (which is what you were referring to in the comment before mine) is well-written.
Greg D
+6  A: 

Reference counting was tried in C#. I believe, the folks that released Rotor (a reference implementation of CLR for which the source was made available) did reference counting-based GC just to see how it would compare to the generational one. The result was surprising -- the "stock" GC was so much faster, it was not even funny. I don't remember exactly where I heard this, I think it was one of the Hanselmuntes podcasts. If you want to see C++ get basically crushed in performance comparison with C# -- google Raymond Chen's chinese dictionary app. He did a C++ version, and then Rico Mariani did a C# one. I think it took Raymond 6 iterations to finally beat the C# version, but by that time he had to drop all the nice object orientednes of C++, and get down to the win32 API level. The entire thing turned into a performance hack. C# program, at the same time, was optimized only once, and in the end still looked like a decent OO project

Reference counting is known to be slower overall than tracing GCs. However, it has the benefit of lower maximum pause times, and memory will often be released as soon as possible.
Unknown
From memory I don't think the Rotor runtime was very optimised so I don't think it's a very good data point. +1 for the dictionary app reference that was interesting. Here is the link for reference: http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx
Luke Quinane
I feel the need to point out a non-trivial amount of the effort Mr. Chen had to go to was due to the fact that the standard C++ library - or maybe just most current implementations - is crap for performance (being designed for a "sufficiently intelligent optimizer") - it would be perfectly possible to write an as efficient program in OO style.
Simon Buchan
@Simon: Be that as it may, the fact remains that with currently-available tools, it was much easier, faster, and less error-prone to write c# code that performed well than it was to write c++ code that performed well.
Greg D
+4  A: 

I know something about garbage collection. Here is a short summary because a full explanation is beyond the bounds of this question.

.NET uses a copying and compacting generational garbage collector. This is more advanced than reference counting and has the benefit of being able to collect objects that refer to themselves either directly, or through a chain.

Reference counting will not collect cycles. Reference counting also has a lower throughput (slower overall) but with the benefit of faster pauses (maximal pauses are smaller) than a tracing collector.

Unknown
This point could be argued but it seems easier to avoid a cycle in a reference counted system using weak references than it is to avoid resource leaks or delayed release with IDisposable.
Skrymsli
@Skrymsli actually that point cannot be argued. Sometimes you don't know when you'll create a reference cycle.
Unknown
Only naïve reference counting implementations will not collect cycles. High performance reference counting is also possible for runtimes such as .net: http://cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf
Luke Quinane
@Luke, that is incorrect. The paper you cite is a misnomer. Their "ulterior reference counting" implements tracing, which by standard definitions, is not a pure reference counting GC.
Unknown
You are correct, it is a hybrid MS/RC collector, however it only uses MS for the nusery. I.e. it is a mostly reference counting garbage collector. Also many RC implementations use MS to collect cycles.
Luke Quinane
+1  A: 

The object implemeting IDisposable must also implement a finalizer called by the GC when the user doesn't explicit call Dispose - see IDisposable.Dispose at MSDN.

The whole point of IDisposable is that the GC is running at some non-deterministic time and you implement IDisposable because you hold a valuable resource and wants to free it at a deterministic time.

So your proposal would change nothing in terms of IDisposable.

Edit:

Sorry. Didn't read your proposal correctly. :-(

Wikipedia has a simple explanation of the shortcomings of References counted GC

mlarsen
+2  A: 

There's a lot of issues in play here. First of all you need to distinguish between freeing managed memory and clean-up of other resources. The former can be really fast whereas the later may be very slow. In .NET the two are separated, which allows for faster clean-up of managed memory. This also implies, that you should only implement Dispose/Finalizer when you have something beyond managed memory to clean up.

The .NET employs a mark and sweep technique where it traverses the heap looking for roots to objects. Rooted instances survive the garbage collection. Everything else can be cleaned by just reclaiming the memory. The GC has to compact memory every now and then, but apart from that reclaiming memory is a simple pointer operation even when reclaiming multiple instances. Compare this with multiple calls to destructors in C++.

Brian Rasmussen
+1 for noting the two separate aspects. People tend to think Dispose() is for freeing up memory...
Lucas
+2  A: 

Reference count

The costs of using reference counts are twofold: First, every object requires the special reference count field. Typically, this means an extra word of storage must be allocated in each object. Second, every time one reference is assigned to another, the reference counts must be adjusted. This increases significantly the time taken by assignment statements.

Garbage Collection in .NET

C# does not use reference counting of the objects. Instead it maintains a graph of the object references from the stack and navigates from the root to cover up all the referenced objects. All the referenced objects in the graph are compacted in the heap to that a contiguous memory is available for future objects. Memory for all the unreferenced objects who do not need to be finalized is reclaimed. Those that are unreferenced but have finalizers to be executed on them are moved to a separate queue called the f-reachable queue where the garbage collector calls their finalizers in the background.

In addition to the above GC uses the concept of generations for a more efficient garbage collection. It is based on the following concepts 1. It is faster to compact the memory for a portion of the managed heap than for the entire managed heap 2. Newer objects will have shorter lifetimes and older objects will have longer lifetimes 3. Newer objects tend to be related to each other and accessed by the application around the same time

The managed heap is divided into three generations: 0, 1, and 2. The new objects are stored in gen 0. Objects that are not reclaimed by a cycle of GC are promoted to the next gen. So if newer objects which are in gen 0 survive GC cycle 1, then they are promoted to gen 1. Those among these that survive GC cycle 2 are promoted to gen 2. Because the garbage collector supports only three generations, objects in generation 2 that survive a collection remain in generation 2 until they are determined to be unreachable in a future collection.

The garbage collector performs a collection when generation 0 is full and memory for new object needs to be allocated. If a collection of generation 0 does not reclaim enough memory, the garbage collector can perform a collection of generation 1, then generation 0. If this does not reclaim enough memory, the garbage collector can perform a collection of generations 2, 1, and 0.

Thus GC is more efficient than reference count.

Rashmi Pandit
Technically, it's a graph, not a tree, and the GC doesn't "maintain" it, it simply walks the references inside objects. Also, I'm not sure what you mean by "removal by the [GC] in the background" - do you mean the sweep phase? I think that immediately follows.
Simon Buchan
Thanks Simon. Edited the response as per your suggestion. I am not sure what the sweep phase is, what i meant was the objects out of scope who need finalization are processed separately by the GC in the background after the heap is compacted.
Rashmi Pandit
+2  A: 

There is a difference between C++ style smart pointer reference counting and reference counting garbage collection. I've also talked about the differences on my blog but here is a quick summary:

C++ Style Reference Counting:

  • Unbounded cost on decrement: if the root of a large data structure is decremented to zero there is an unbounded cost to free all the data.

  • Manual cycle collection: to prevent cyclic data structures from leaking memory the programmer must manually break any potential structures by replacing part of the cycle with a weak smart pointer. This is another source of potential defects.

Reference Counting Garbage Collection

  • Deferred RC: Changes to an object reference count are ignored for stack and register references. Instead when a GC is triggered these objects are retained by collecting a root set. Changes to the reference count can be deferred and processed in batches. This results in higher throughput.

  • Coalescing: using a write barrier it is possible to coalesce changes to the reference count. This makes it possible to ignore most changes to an objects reference count improving RC performance for frequently mutated references.

  • Cycle Detection: for a complete GC implementation a cycle detector must also be used. However it is possible to perform cycle detection in incremental fashion, which in turn means bounded GC time.

Basically it is possible to implement a high performance RC based garbage collector for runtimes such as Java's JVM and the .net CLR runtime.

I think tracing collectors are partly in use for historical reasons: many of the recent improvements in reference counting came after both the JVM and .net runtime were released. Research work also takes time to transition into production projects.

Deterministic Resource Disposal

This is pretty much a separate issue. The .net runtime makes this possible using the IDisposable interface, example below. I also like Gishu's answer.


@Skrymsli, this is the purpose of the "using" keyword. E.g.:

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // No need to call finalizer now
    }

    protected virtual void Dispose(bool disposing) { }
}

Then to add a class with a critical resource:

public class ComFileCritical : BaseCriticalResource {

    private IntPtr nativeResource;

    protected override Dispose(bool disposing) {
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}

Then to use it is as simple as:

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point

See also implementing IDisposable correctly.

Luke Quinane
Interesting blog post... I'm less concerned about freeing the memory than freeing non-memory resources which are potentially time-critical. (Think of a handle to a COM object that holds a file lock. You want that lock to be released when you are done with it, not when the GC runs a finalizer at some future time.) I think there has to be some combination of GC and smart pointers that provides the best of both worlds where you get exceptional memory management and yet critical resources can be deterministically freed without undue burden on the programmer.
Skrymsli
When I ask about this is isn't because I don't know about using or the right way to dispose of resources. I just think its WRONG that we have to do it at all! It usually works out as you described above. What if you need to hold on to that resource? You need to implement IDisposable now. Also, the programmer has to know to wrap the stuff in the using construct. (I know, use FXCop!) That's just syntactic sugar for calling free, release, dispose. The compiler should *know* to do it. This is the major bonus of the C++ RAII idiom. Sorry, I'm probably just dumb.
Skrymsli
@Skrymsli you are not alone in your "dumbness", I also suggested it several years ago: http://www.dotnet247.com/247reference/msgs/26/131667.aspx
alpav
+3  A: 

Brad Abrams posted an e-mail from Brian Harry written during development of the .Net framework. It details many of the reasons reference counting was not used, even when one of the early priorities was to keep semantic equivalence with VB6, which uses reference counting. It looks into possibilities such as having some types ref counted and not others (IRefCounted!), or having specific instances ref counted, and why none of these solutions were deemed acceptable.

Because [the issue of resource management and deterministic finalization] is such a sensitive topic I am going to try to be as precise and complete in my explanation as I can. I apologize for the length of the mail. The first 90% of this mail is trying to convince you that the problem really is hard. In that last part, I'll talk about things we are trying to do but you need the first part to understand why we are looking at these options.

...

We initially started with the assumption that the solution would take the form of automatic ref counting (so the programmer couldn't forget) plus some other stuff to detect and handle cycles automatically. ...we ultimately concluded that this was not going to work in the general case.

...

In summary:

  • We feel that it is very important to solve the cycle problem without forcing programmers to understand, track down and design around these complex data structure problems.
  • We want to make sure we have a high performance (both speed and working set) system and our analysis shows that using reference counting for every single object in the system will not allow us to achieve this goal.
  • For a variety of reasons, including composition and casting issues, there is no simple transparent solution to having just those objects that need it be ref counted.
  • We chose not to select a solution that provides deterministic finalization for a single language/context because it inhibits interop with other languages and causes bifurcation of class libraries by creating language specific versions.
Lucas