Recently read in a article on dotnetpearls.com here saying that static ctors take a substantial amount of perfomance hit.
Could not fathom why?
I just did a small test to check the impact of adding a static constructor to one of my classes.
I have a base class that looks like this:
public abstract class Base
{
public abstract Task DoStuffAsync();
}
The problem is, that in one of the implementations that method does nothing, so I can set a pre-made completed task and return it every time.
public sealed class Test1 : Base
{
readonly Task _emptyTask;
public Test1()
{
TaskCompletionSource<Object> source = new TaskCompletionSource<object>();
source.SetResult(null);
_emptyTask = source.Task;
}
public override Task DoStuffAsync()
{
return _emptyTask;
}
}
(Other option is to return the task on demand, but turns out this method is always called)
Objects of this class are created very very often, usually in loops. Looking at it, it looks like setting _emptyTask
as a static field would be beneficial since it would be the same Task
for all methods:
public sealed class Test2 : Base
{
static readonly Task _emptyTask;
static Test2()
{
TaskCompletionSource<Object> source = new TaskCompletionSource<object>();
source.SetResult(null);
_emptyTask = source.Task;
}
public override Task DoStuffAsync()
{
return _emptyTask;
}
}
Then I remember the "issue" with static constructors and performance, and after research a little (that is how I get here), I decide to do a small benchmark:
Stopwatch sw = new Stopwatch();
List<Int64> test1list = new List<Int64>(), test2list = new List<Int64>();
for (int j = 0; j < 100; j++)
{
sw.Start();
for (int i = 0; i < 1000000; i++)
{
Test1 t = new Test1();
if (!t.DoStuffAsync().IsCompleted)
throw new Exception();
}
sw.Stop();
test1list.Add(sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
Test2 t = new Test2();
if (!t.DoStuffAsync().IsCompleted)
throw new Exception();
}
sw.Stop();
test2list.Add(sw.ElapsedMilliseconds);
sw.Reset();
GC.Collect();
}
Console.WriteLine("Test 1: " + test1list.Average().ToString() + "ms.");
Console.WriteLine("Test 2: " + test2list.Average().ToString() + "ms.");
And the results are quite clear:
Test 1: 53.07 ms.
Test 2: 5.03 ms.
end
So despite of having a static constructor, the benefit outweighs the issue. So always measure.
Well I've just replicated his test.
For 1000000000 iterations with a DEBUG build I get:
The same with a RELEASE build does highlight a difference:
The CLR provides a pretty strong guarantee for the execution of static constructors, it promises to call them only once and before any method in the class can run. That guarantee is fairly tricky to implement when there are multiple threads using the class.
Taking a peek at the CLR source code for SSCLI20, I see a fairly large chunk of code dedicated to providing this guarantee. It maintains a list of running static constructors, protected by a global lock. Once it gets an entry in that list, it switches to a class specific lock which ensures no two threads can be running the constructor. Double-checked locking on a status bit that indicates that the constructor was already run. Lots of inscrutable code that provides exception guarantees.
Well, this code doesn't come for free. Add it to the execution time for the cctor itself and you're looking at some overhead. As always, don't let this cramp your style, this guarantee is also a very nice one that you wouldn't want to provide yourself. And measure before you fix.
I think "substantial" is an overstatement in most use cases.
Having a static constructor (even if it does nothing) affects type initialization time due to the presence/absence of the beforefieldinit flag. There are stricter guarantees about timing when you have a static constructor.
For most code, I'd suggest this doesn't make much difference - but if you're tight-looping and accessing a static member of a class, it might. Personally I wouldn't worry about it too much - if you have a suspicion that it's relevant in your real application, then test it rather than guessing. Microbenchmarks are very likely to exaggerate the effect here.
It's worth noting that .NET 4 behaves somewhat differently to previous versions when it comes to type initialization - so any benchmarks should really show the different versions in order to be relevant.