Something I heard yesterday made me think about the aspect of performance, when deciding whether to use interfaces or delegates to implement a specific feature. For example, when implementing the Observer pattern, the classic approach involves interfaces, while .NET has support for (multicast) delegates natively, suggesting a builtin approach that might make more sense in a .NET language.

To make this decision, there are of course many arguments to consider — most importantly, when talking about design patterns, recognisability is an important factor that might be better served by following the classic approach. Nevertheless, .NET delegates and events are more flexible and easier to implement, so I figured that recognisability might not have the final word here. In some cases, the performance of the two respective approaches might not be very important. These days other aspects of software development are often more important than performance-based evaluations.

Still, I figured that the difference between the two could potentially be large, mainly because the management overhead of the multicast delegate might be a lot higher than the simple call into a known interface. What’s more, patterns like the Observer are sometimes implemented in situations where fast data update is essential. So I created a test scenario where a billion calls were made for each of the two approaches, measuring the time this takes. I implemented the test so that each run was repeated five times and the average duration of each run was then shown, to provide for a little tolerance towards the fact that my system is running other programs at the same time as the test. I compiled debug and release versions of this program for each VS 2005 (beta 2) and VS 2003, and I included a dummy test that only runs an empty loop with the same number of iterations, to be able to spot instances where the compiler might optimise away parts of my test code. Here are the results:

Several things look interesting about these results:

  • It looks like the debug executable for .NET 2 runs a bit slower than the one for .NET 1 does. Maybe not that surprising, it’s still a beta after all.
  • The empty loop has not been eliminated by any of the two compilers, even in release mode it still does its cycles.
  • At least in release mode, .NET 2 has an edge over .NET 1 when calling interface methods.
  • And finally, the speed of calling delegates in .NET 2 is absolutely incredible! Not only is it nearly three times as fast as in .NET 1, it’s also faster than the calls to the interface.

Seems like the result is this: if you’re using .NET 2, don’t fear going with delegates because of speed issues. If you’re still on .NET 1 and speed is a really important factor for you (don’t forget we’re talking about a billion calls here), you might want to consider using interfaces instead — or just go to .NET 2 as soon as possible. Here’s the code of the test I used, in case someone wants to reproduce my findings:

using System;

namespace CallInterfaceDelegateSpeedTest {
  class Class1 {
    [STAThread]
    static void Main(string[] args) {
      callTarget = new CallTarget( );
      iCallTarget = (ICallTarget) callTarget;
      delegateCallTarget = new CallMethod(callTarget.Go);

      Test("Empty loop", new TestMethod(TestEmptyLoop));
      Test("Interface calls", new TestMethod(TestInterfaceCalls));
      Test("Delegate calls", new TestMethod(TestDelegateCalls));

      Console.ReadLine( );
    }

    static CallTarget callTarget;
    static ICallTarget iCallTarget;
    static CallMethod delegateCallTarget;

    delegate void CallMethod( );
    delegate void TestMethod( );
    const long runCount = 1000000000;
    const int compareCount = 5;

    static void Test(string testName, TestMethod testMethod) {
      Console.Out.WriteLine("Running test '" + testName + "'");

      TimeSpan completeDuration = TimeSpan.Zero;
      for (int i = 0; i < compareCount; i++) {
        DateTime startTime = DateTime.Now;
        testMethod( );
        completeDuration += (DateTime.Now - startTime);
      }

      Console.Out.WriteLine("Finished {0} runs in {1}, average duration {2}",
        compareCount, completeDuration,
        TimeSpan.FromSeconds(completeDuration.TotalSeconds / compareCount));
    }

    static void TestInterfaceCalls( ) {
      for (long j = 0; j < runCount; j++)
        iCallTarget.Go( );
    }

    static void TestDelegateCalls( ) {
      for (long j = 0; j < runCount; j++)
        delegateCallTarget( );
    }

    static void TestEmptyLoop( ) {
      for (long j = 0; j < runCount; j++) { }
    }
  }

  public class CallTarget : ICallTarget {
    public void Go( ) { }
  }

  public interface ICallTarget {
    void Go( );
  }
}