Friday, June 12, 2009

Why static methods are preferred?

As the books and code analysis tools like FxCop suggests us to use static methods as far as possible, unfortunately, like many other such guidance, there is hardly any explanation why it should be like that. Today while searching for something on my PC, I came across this note of mine – "Explore why static methods are preferred". While I didn't find what I was searching for in the first place (and found something else instead, thus proving one of the Murphie's Laws, QED), I was delighted to be reminded of this puzzle.

Quickly, I wrote down this small piece of program:

class Program

{

static void Main(string[] args)

{

//call the static method

Foo.FooStatic();

//create instance of Foo

Foo instance = new Foo();

//call virtual method

instance.FooVirtual();

//call non-virtual method

instance.FooNonVirtual();

}

//our Foo class

internal class Foo

{

//static method

public static void FooStatic()

{

}

//virtual method

public virtual void FooVirtual()

{

}

//non-virtual method

public void FooNonVirtual()

{

}

}

}

There are three methods in the Foo class, one of them being a static method. The rest are designed to be member methods – virtual and non-virtual. After compilation, I dissected it to find out how CLR would call them. Here's the disassembly:

IL_0000: nop

IL_0001: call void Blog20090612.Program/Foo::FooStatic()

IL_0006: nop

IL_0007: newobj instance void Blog20090612.Program/Foo::.ctor()

IL_000c: stloc.0

IL_000d: ldloc.0

IL_000e: callvirt instance void Blog20090612.Program/Foo::FooVirtual()

IL_0013: nop

IL_0014: ldloc.0

IL_0015: callvirt instance void Blog20090612.Program/Foo: :FooNonVirtual()

IL_001a: nop

IL_001b: ret

We can see that for a static non-member method, the compiler uses call. One of the differences between a call and a callvirt is that the latter uses a null-check on the class instance. Makes sense. The instance must exist before you can call its method (though strictly speaking, for an object oriented compiler, this is not a must). If the instance is null, it should throw a NullReferenceException. This clearly indicates some performance degradation when calling instance methods when we are sure that the instance is not null. Hence, it is a good idea to make the methods static as far as possible even though the gain is not significantly noticeable.

As is my habit, I never leave a chance to do a what..if check on anything I learn. The question that stormed my mind is now: is there a way for us, the mortal developers, to force the compiler to use call instead of callvirt even for instance methods?

But before I do that, my curious fingers added the following line in the FooNonVirtual() method:

//non-virtual method

public void FooNonVirtual()

{

base.ToString();

}

And the disassembly now looks like:

IL_0000: nop

//000041: base.ToString();

IL_0001: ldarg.0

IL_0002: call instance string [mscorlib]System.Object::ToString()

IL_0007: pop

//000042: }

IL_0008: ret

Wo..wo..what's that? ToString() being an instance method, isn't the compiler supposed to generate a callvirt just as we learnt? Well…to think about it, that would have been a silly way. Sitting inside an instance, there is no point in checking if our base class is null or not, right? So the compiler uses call – the one that doesn't check for null instance. Fascinating!

Coming back to my idea of forcing the compiler to skip the null instance check for an instance method, I tried the following:

static void Main(string[] args)

{

//create instance of Foo

Foo instance = new Foo();

//call instance method through instance variable

instance.FooVirtual();

//call instance method with a trick

new Foo().FooVirtual();

}

And the IL emitted by the compiler:

IL_0000: nop

IL_0001: newobj instance void Blog20090612.Program/Foo::.ctor()

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: callvirt instance void Blog20090612.Program/Foo::FooNonVirtual()

IL_000d: nop

IL_000e: newobj instance void Blog20090612.Program/Foo::.ctor()

IL_0013: call instance void Blog20090612.Program/Foo::FooNonVirtual()

IL_0018: nop

IL_0019: ret

Bingo! Because a new will never return a null, the compiler now decides to use a call even for an instance method.

Just one point before I finish, this won't be applicable for virtual methods, because callvirt is essentially used for all the hard work before it locates and calls a virtual method. In other words, virtual methods can never be called using call. Or can we? Give me some more time J

2 comments:

  1. Thanks Amit for a wonderful article. It's very nice how you drive your point home. Crystal clear!

    Keep writing - I am subscribing to your feed.

    Kamal Bisht

    ReplyDelete
  2. Great Amit. Long wait now finished! Hope to learn more from you.

    Srihari

    ReplyDelete