Re: About speed
- From: Barry Kelly <barry.j.kelly@xxxxxxxxx>
- Date: Sun, 25 Jun 2006 19:19:11 +0100
Robert Giesecke <Spam@xxxxxxxxx> wrote:
Most IDisposables have a finalizer, that will call Dispose when the
object is marked on a GC sweep.
This isn't right. It is *extremely* rare that a class implementing
IDisposable needs a finalizer, and most code that I see that implements
a finalizer is actually incorrectly implemented. (Even some classes in
the .NET libraries implement it incorrectly, especially in WinForms.)
The finalizer pattern looks like this:
---8<---
class T : IDisposable
{
~T() { Dispose(false); }
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
// dispose *managed* resources
// dispose *unmanaged* resources
}
}
--->8---
From this, you can see that using the finalizer pattern on a class whichisn't wrapping an unmanaged resource does *nothing* - it wastes time on
the GC finalizer thread, and wastes resources keeping track of the
finalizable object.
Every object in the .NET BCL which wraps an OS primitive already
implements a finalizer, or points to another managed object which does:
Semaphore, ManualResetEvent, FileStream, etc. (they usually use
SafeHandle descendants in .NET 2.0, which use critical finalizer
support, so that Thread.Abort won't leak resources).
Contained objects of these types (i.e. managed resources like FileStream
etc.) should be disposed of in the "if (disposing)" branch above, but
they don't need to be disposed of in the "dispose *unmanaged* resources"
section above, because they themselves are managed resources - their own
finalizers will take care of finalization.
Typically, if you're using P/Invoke and have a field of your class of
type IntPtr to hold some kind of handle, then you need to consider using
the finalizer pattern. If you're not, then you don't.
But those IDisposables will most likely
call GC.SupressFinalizer inside their Dispose method. Thus using "using"
will prevent finalizers from being executed. Finalizers can slow things
extremely down, because they to be synchronized as the GC runs in its
own thread.
You're right about finalizable objects being significantly more
expensive to collect. Check out the program I appended to this post. It
has definitions for two classes, one of which inherits from the other.
The base class implements the IDisposable pattern and the second one
implements the Finalizer pattern. Chains of both objects are built and
then (1) disposed explicitly (using IDispose), and (2) discarded.
These are the results (raw data) I get on my system:
Disposable (Dispose) 0.147
Disposable (Dispose) 0.152
Disposable (Dispose) 0.152 Average 0.156
Disposable (Dispose) 0.177 StdDev 0.011937336
Disposable (Dispose) 0.152 Range 0.03
Finalizer (Dispose) 1.034
Finalizer (Dispose) 1.031
Finalizer (Dispose) 1.032 Average 1.0354
Finalizer (Dispose) 1.04 StdDev 0.004335897
Finalizer (Dispose) 1.04 Range 0.009
Disposable (discarded) 0.055
Disposable (discarded) 0.054
Disposable (discarded) 0.054 Average 0.0544
Disposable (discarded) 0.054 StdDev 0.000547723
Disposable (discarded) 0.055 Range 0.001
Finalizer (discarded) 2.683
Finalizer (discarded) 2.672
Finalizer (discarded) 2.681 Average 2.6792
Finalizer (discarded) 2.692 StdDev 0.009471008
Finalizer (discarded) 2.668 Range 0.024
The reason a discarded object with a finalizer is so much more expensive
than one which had Dispose() called on it is that GC.SuppressFinalize()
takes it off the finalization queue. It's still not cheap, though.
(Sidenote: I know my terminology isn't totally standard with
IDisposable/Dispose pattern versus Finalizer pattern, but the .NET
documentation conflates the two issues, when they are actually distinct
yet related. All objects with a Finalizer should implement IDisposable;
but very few objects implementing IDisposable should implement a
Finalizer. A good blog entry on this dichotomy with lots more pointers
is here:
http://dotnetjunkies.com/WebLog/rajchaniansbiztalkblog/archive/2005/04/20/69429.aspx)
> And you are right. There is no way to free objects the Delphi way as
> far as I know.
It is possible, but I won't explain how. Last time I did, I had to
explain that someone why he shoot himself in the foot. You only need it,
if your design is fundamentally flawed. But then, you'd have to redesign
anyway. ;-)
One can use the Marshal class to allocate unmanaged memory (or
alternatively P/Invoke another memory manager), but then you have to use
IntPtr and unsafe code to manipulate it - you can't instantiate a
managed class over unmanaged memory.
A not-as-nasty-but-still-flawed solution would be to call GC.Collect
twice. Thus every non-collected object will be gen 1 after the first
collect and since it wasn't touched between the 1st and the 2nd, it will
be free'd after the 2nd Collect.
Actually, explicitly calling GC.Collect() once causes a full collection
of all generations. You have to call the other overload,
GC.Collect(int), if you don't want to collect all generations.
This *could* be necessary if you won't leave enough resources for the GC
Thread to do its job. In a standard user-mode application, GC will run
in a Background thread. Thus if you do a lot of crunching, GC will sweep
only if you've eaten *lots* of memory.
This isn't true. There's only a GC thread (per CPU (= per heap)) for
Server-mode GC; Workstation GC does its collection work on the thread
that caused the GC (usually because a memory allocation exceeded the
gen0 threshold).
Even for Server, it isn't quite right. GC is triggered in one of three
conditions:
1) Allocation exceeds the Gen0 threshold;
2) System.GC.Collect is called;
3) System is in low memory situation;
So, if you're doing a lot of crunching that's doing a lot of allocation,
then the GC will still be triggered by an allocation, and isn't delayed
until the OS interrupts the thread due to quantum exhaustion - there's
no risk of "eating lots of memory" unless you're keeping all your
allocations unnecessarily rooted in the stack or heap somewhere.
(In a standard user-mode application, you're end up using the
workstation GC).
Good blog entries for the GC implementation:
http://blogs.msdn.com/maoni/archive/2004/06/15/156626.aspx
http://blogs.msdn.com/maoni/archive/2004/09/25/234273.aspx
http://blogs.msdn.com/maoni/archive/2006/02/28/541095.aspx
http://blogs.msdn.com/clyon/archive/2004/09/08/226981.aspx
I also recommend this PDC presentation:
http://microsoft.sitestream.com/PDC05/FUN/FUN421_files/intro.htm#nopreload=1&autostart=1
Finalizable / non-finalizable test program follows:
---8<---
using System;
using System.Diagnostics;
class App
{
class WithFinalizer : JustDisposable
{
public WithFinalizer()
{
}
protected override void Dispose(bool disposing)
{
if (disposing)
GC.SuppressFinalize(this);
base.Dispose(disposing);
}
~WithFinalizer()
{
Dispose(false);
}
}
class JustDisposable : IDisposable
{
public JustDisposable()
{
}
void IDisposable.Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
if (Link != null)
((IDisposable) Link).Dispose();
}
public JustDisposable Link;
}
delegate void Method();
static void Benchmark(int reps, string label, Method method)
{
method();
Stopwatch watch = Stopwatch.StartNew();
for (int i = 0; i < reps; ++i)
method();
Console.WriteLine("{0}, {1:f3}, {2} reps",
label,
watch.ElapsedTicks / (double) Stopwatch.Frequency,
reps);
}
static WithFinalizer CreateWithFinalizerChain(int length)
{
WithFinalizer result = null;
for (int i = 0; i < length; ++i)
{
WithFinalizer added = new WithFinalizer();
added.Link = result;
result = added;
}
return result;
}
static JustDisposable CreateJustDisposableChain(int length)
{
JustDisposable result = null;
for (int i = 0; i < length; ++i)
{
JustDisposable added = new JustDisposable();
added.Link = result;
result = added;
}
return result;
}
static void Main()
{
const int innerReps = 50000;
const int outerReps = 5;
for (int i = 0; i < outerReps; ++i)
Benchmark(innerReps, "Disposable (Dispose)", delegate
{
((IDisposable)
CreateJustDisposableChain(100)).Dispose();
});
for (int i = 0; i < outerReps; ++i)
Benchmark(innerReps, "Finalizer (Dispose)", delegate
{
((IDisposable) CreateWithFinalizerChain(100)).Dispose();
});
for (int i = 0; i < outerReps; ++i)
Benchmark(innerReps, "Disposable (discarded)", delegate
{
CreateJustDisposableChain(100);
});
for (int i = 0; i < outerReps; ++i)
Benchmark(innerReps, "Finalizer (discarded)", delegate
{
CreateWithFinalizerChain(100);
});
}
}
--->8---
-- Barry
--
http://barrkel.blogspot.com/
.
- Follow-Ups:
- Re: About speed
- From: Alex
- Re: About speed
- References:
- Re: About speed
- From: Árpád Soós
- Re: About speed
- From: Oliver Townshend
- Re: About speed
- From: Don Strenczewilk
- Re: About speed
- From: Bryce K. Nielsen
- Re: About speed
- From: Craig Stuntz [TeamB]
- Re: About speed
- From: Bryce K. Nielsen
- Re: About speed
- From: Ingvar Nilsen
- Re: About speed
- From: Bryce K. Nielsen
- Re: About speed
- From: Ingvar Nilsen
- Re: About speed
- From: Bryce K. Nielsen
- Re: About speed
- From: Joanna Carter [TeamB]
- Re: About speed
- From: Ingvar Nilsen
- Re: About speed
- From: Robert Giesecke
- Re: About speed
- Prev by Date: Re: About speed
- Next by Date: Re: About speed
- Previous by thread: Re: About speed
- Next by thread: Re: About speed
- Index(es):
Relevant Pages
|
|