Wednesday, February 4, 2009

Virtual methods, interfaces and the strategy pattern

The other day while refactoring some code that was called quite often I thought “Oh, this looks like a candidate for a strategy”. Here is a simplified version of the code:

   1: int a = 0;
   2: for (int i = 0; i < count; i++)
   3: {
   4:     if (this.SomeBoolProperty)
   5:     {
   6:         a += 2;
   7:     }
   8:     else
   9:     {
  10:         a += 1;
  11:     }
  12:     //a += (this.SomeBoolProperty) ? 2 : 1;
  13: }


And what I wanted to do is something like:

   1: public virtual bool SomeBoolProperty
   2: {
   3:     get
   4:     {
   5:         return this.boolValue;
   6:     }
   7:     set
   8:     {
   9:         if (this.boolValue != value)
  10:         {
  11:             this.boolValue = value;
  12:             if (this.boolValue)
  13:             {
  14:                 this.calculatorVirtual = new TrueCalculator();
  15:             }
  16:             else
  17:             {
  18:                 this.calculatorVirtual = new FalseCalculator();
  19:             }
  20:         }
  21:     }
  22: }
   1: int a = 0;
   2: ParameterClass param = new ParameterClass();
   3: for (int i = 0; i < count; i++)
   4: {
   5:     a += this.calculatorVirtual.TestMethod(param);
   6: }

I was just about to start refactoring that piece of code into a couple of little strategy classes when my inner voice asked: “Wait, will it really be more effective?”. Why is this you ask. Because virtual method calls can be expensive, and if statements are supposed to be cheap.

But I didn’t stop there, I wanted to know just how much each option would cost me. And I prepared a simple program to test the two options. I even added a third option. I thought “If virtual method are so expensive, let’s not use them, we’ll use interfaces and the problem will be gone.” Interfaces don’t need virtual methods to work and so there won’t be any virtual tables involved, right? It turned out that I couldn't be more wrong.

The results speak for themselves:

PerformanceTest

It turns out that just adding the virtual keyword before a method or a property makes it’s call about three times more expensive. The other interesting thing is that interface method calls are about 1,5 times more expensive than virtual method calls. Even though the IL code that is generated is the same. I wonder what is the reason for this.

Here is the code that starts the tests, in each test method there is code similar to the code above, count = 100 000 000:

   1: private TestClass testClass = new TestClass();
   2:  
   3: private void btnTestIf_Click(object sender, EventArgs e)
   4: {
   5:     this.RunTest(new Action(this.testClass.RunIfTest), this.lblTimeIf);
   6: }
   7:  
   8: private void btnTestVirtualIf_Click(object sender, EventArgs e)
   9: {
  10:     this.RunTest(new Action(this.testClass.RunVirtualIfTest), this.lblTimeVirtualIf);
  11: }
  12:  
  13: private void btnTestVirtual_Click(object sender, EventArgs e)
  14: {
  15:     this.RunTest(new Action(this.testClass.RunVirtualTest), this.lblTimeVirtual);
  16: }
  17:  
  18: private void btnTestInterface_Click(object sender, EventArgs e)
  19: {
  20:     this.RunTest(new Action(this.testClass.RunInterfaceTest), this.lblTimeInterface);
  21: }
  22:  
  23: public void RunTest(Action testMethod, Label label)
  24: {
  25:     int start = Environment.TickCount;
  26:     testMethod();
  27:     int end = Environment.TickCount;
  28:  
  29:     label.Text = string.Format("{0} ms", (end - start).ToString());
  30: }

Finally, here are my key take aways:

  1. Use the virtual keyword wisely. Only when you need it.
  2. Use base classes with virtual methods. Especially if you have some common logic that you can put in the base class.
  3. In most cases replacing a sequence of if statements that is used in many places with a strategy / template method is worth it because it makes the code much more cleaner, easier to understand and change.

That was all for now, see you next time

Yordan, the curious

No comments: