3

Assuming the following:

  • A class has managed only members.
  • Some members implement IDisposable.
  • The class is sealed - a class can't derive from and add unmanaged resources.
  • The object is used inside a using statement - i.e Dispose() is called when done.

There are 3 possible implementations of IDisposable for this class:

  1. Minimal Dispose method that calls Dispose() on IDisposable members - NO finalizer.
  2. The standard IDisposable implementation with Finalizer BUT missing the usual GC.SuppressFinalize(this) call in Dispose().
  3. The full standard IDisposable implementation with Finalizer (and with GC.SuppressFinalize(this) call in Dispose()).

Are the following statements correct? Have I understood this correctly?

  1. Case A. is has slightly less overhead than B. and C. because the object does not have a finalizer so it does not go in the GCs finalization queue - because of that the GC can clean this object early on in collection - no overhead.
  2. Case B. the object has a finalizer so will end up in the GCs finalizer queue and the finalizer will get call (because it wasn't suppressed) - the finalizer calls dispose which does nothing because its been called already. This incurs small overhead of object being in finalizer queue and the very small overhead of finalizer call.
  3. Case C. the object has a finalizer so will still end up in the GCs finalizer queue. Because the dispose and hence SuppressFinalize has been called the finalizer won't run. This case still incurs small overhead of the object going in the finalizer queue but the finalizer doesn't actually run.

The key point here that it is tempting to think that "I've avoided the finalizer overhead by calling SuppressFinalize" - but I think (and would like to clarify) that that is incorrect. The overhead of the object being in the finalizer queue IS still incurred - all you are avoiding is the actual finalizer call - which in the common case is just the "I'm disposed already do nothing".

Note: Here by "Full standard IDisposable implementation" I mean the standard implementation that is designed to cover both the unmanaged and managed resources case (note here we only have managed object members).

public void Dispose() {
    Dispose(true);
    GC.SuppressFinalize(this);
}

private bool _disposed;
protected virtual void Dispose(bool disposing) {
if (_disposed)
    return;
    if (disposing) {
        // dispose managed members...
    }
    _disposed = true;
}

~AXCProcessingInputs() {
    Dispose(false);
}
Ian R. O'Brien
  • 6,682
  • 9
  • 45
  • 73
Ricibob
  • 7,505
  • 5
  • 46
  • 65
  • Obligatory comment stating that "You must never assume a finalizer will be called", Addendum, the circumstances that can cause a finalizer to *not* be called are legion; see http://blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx – Binary Worrier Mar 19 '15 at 11:05
  • 1
    This is a silly question, a class with only managed members must never have a finalizer. In fact, no C# class should ever have a finalizer, that was a .NET 1.x mistake. Elegantly fixed in 2.0 with the SafeHandle classes and their critical finalizers. Trying to reason a "what could happen" scenario from there is just not useful. Just don't. – Hans Passant Mar 19 '15 at 12:23
  • @Hans Passant:I asked the question because a collegue added a finaizer and claimed "no overhead" because hes calling SuppressFinalize - Im just trying to clarify if that I think there IS an overhead. Im firmly in your no-finalizers-if-no-unmanaged-resources camp - Im just trying to openly resolve a discussion with a collegue – Ricibob Mar 19 '15 at 13:17
  • 3
    @Ricibob perhaps you could just open up one of the files he's working on, and add some pages of code right after a `return` statement. If he asks say there is no overhead because the code will never run. – C.Evenhuis Mar 19 '15 at 14:00
  • Just running the actual finalizer method is certainly not the only cost of not suppressing. *The* largest cost of finalization is that the object needs to survive for a whole additional generation, along with everything it references. Also, there's the cost of context switching to the finalizer thread, but that's probably nitpicking. :) – relatively_random Apr 25 '19 at 15:09

3 Answers3

4

Different versions of the .NET GC may do things differently, but from my understanding any object with a Finalize method will be added to the "finalizer queue" (list of objects which have requested notification if abandoned), and will remain in that queue as long as it exists. The methods to unregister and reregister for finalization (which IMHO should have been protected members of Object) set or clear a flag in the object header which controls whether the object should be moved to the "freachable queue" (list of objects whose finalize method should run as soon as practical) if it is found to be abandoned, but will not cause the object to be added or removed from the finalizer queue.

Every instance of every type that overrides Finalize will thus impose a small but non-zero overhead to every garbage-collection cycle in which it takes part, as long as it exists. Calling SuppressFinalize on an object before abandoning it will prevent it from being moved to the freachable queue, but won't eliminate the overhead resulting from its having been in the finalizable queue throughout its existence.

I would suggest that no public-facing objects should ever implement Finalize. There are some legitimate uses for Finalize methods, but classes that override it should avoid holding references to anything not needed for finalization (somewhat annoyingly, if a finalizable object holds the only reference to a weak reference, the weak reference may be invalidated before the finalizer runs, even if the weak reference's target is still alive).

supercat
  • 77,689
  • 9
  • 166
  • 211
1

You should only include a finalizer on an object that needs to clean up unmanaged resources. Since you only have managed members, you don't need a finalizer - the finalizer will run on the members themselves if they have a finalizer and GC.SuppressFinalize() is not called for them.

The goal of the finalizer is to clean up unmanaged resources along with their managed wrappers whenever the GC feels like it, while the goal of the Dispose pattern is to clean up any type of resource at a specific moment.

No one should think "Ive avoided the finalizer overhead by calling SuppressFinalize" - instead they should think "I've cleaned up my unmanaged resources, there is no need for the finalizer to run".

About the numbered questions:

  1. Yes, having a finalizer run will result in some overhead when it is collected
  2. Yes, calling Dispose() on an already disposed object should be ignored
  3. These classes are added to the finalization queue when an instance is created, but not to the freachable queue when the GC attemots to collect it - it wouldn't make sense to queue an object only to ignore it later. See also this link.
Community
  • 1
  • 1
C.Evenhuis
  • 25,996
  • 2
  • 58
  • 72
  • "Marked finalizable but they are not added to finalization queue" - is that correct? What is the overhead of being marked finalizable compared with actually being in the finalization queue. Does being marked finalizable impact on how efficently the GC can clean up these objects? – Ricibob Mar 19 '15 at 15:10
  • I got confused myself. You were right, they _are_ added to the finalization queue; when GC comes around the object is added to the _freachable_ queue which prevents the object from being immedately collected. The object is not added to this second queue if `SuppressFinalize()` was called. – C.Evenhuis Mar 19 '15 at 15:56
0

I sometimes use finalizers for debugging purposes, to check if I'm missing some dispose somewhere. If anyone is interested, I made a quick test on my system to check the performance impact (Windows 10, .Net 4.7.1, Intel Core i5-8250U).

The cost of adding a finalizer and suppressing it was roughly 60 ns per object, and the cost of adding it and forgetting to call dispose was roughly 800 ns per object. The performance impact was pretty consistent with debug/release builds and with/without the debugger attached, probably because the garbage collector is the same in both builds.

The performance impact of adding a finalizer and suppressing it is minimal unless you're constructing enormous amounts of these objects, which isn't usually the case. Even Microsoft's own Task uses a finalizer (almost always suppressed), and the class is meant to be very lightweight and performant. So they obviously agree.

Letting the finalizer run, however, can get pretty bad. Take into account that my test case used a trivial class with no referenced objects, and it was already an order of magnitude slower. Having lots of referenced objects should cost more, because all of them need to be kept alive for an additional generation. This can also cause lots of copying-around to happen during the compaction phase of the garbage collection.

Source code of the test:

using System;
using System.Diagnostics;

namespace ConsoleExperiments
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            GenerateGarbageNondisposable();
            GenerateGarbage();
            GenerateGarbageWithFinalizers();
            GenerateGarbageFinalizing();

            var sw = new Stopwatch();

            const int garbageCount = 100_000_000;

            for (var repeats = 0; repeats < 4; ++repeats)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageNondisposable();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Non-disposable: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbage();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Without finalizers: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageWithFinalizers();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Suppressed: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageFinalizing();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Finalizing: " + sw.ElapsedMilliseconds.ToString());

                Console.WriteLine();
            }

            Console.ReadLine();
        }



        private static void GenerateGarbageNondisposable()
        {
            var bla = new NondisposableClass();
        }

        private static void GenerateGarbage()
        {
            var bla = new UnfinalizedClass();
            bla.Dispose();
        }

        private static void GenerateGarbageWithFinalizers()
        {
            var bla = new FinalizedClass();
            bla.Dispose();
        }

        private static void GenerateGarbageFinalizing()
        {
            var bla = new FinalizedClass();
        }



        private class NondisposableClass
        {
            private bool disposedValue = false;
        }

        private class UnfinalizedClass : IDisposable
        {
            private bool disposedValue = false;

            protected virtual void Dispose(bool disposing)
            {
                if (!disposedValue)
                {
                    if (disposing)
                    {
                    }

                    disposedValue = true;
                }
            }

            public void Dispose()
            {
                Dispose(true);
            }
        }

        private class FinalizedClass : IDisposable
        {
            private bool disposedValue = false;

            protected virtual void Dispose(bool disposing)
            {
                if (!disposedValue)
                {
                    if (disposing)
                    {
                    }

                    disposedValue = true;
                }
            }

            ~FinalizedClass()
            {
                Dispose(false);
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }
    }
}
relatively_random
  • 4,505
  • 1
  • 26
  • 48