Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Can calling a delegate be as fast as calling a virtual method?
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using static System.Console;
namespace TestPerf
{
interface IInter
{
void Method();
}
class BaseClass : IInter
{
[MethodImpl(MethodImplOptions.NoInlining)]
public void Method() {}
[MethodImpl(MethodImplOptions.NoInlining)]
public virtual void VirtualMethod() {}
}
class DerivedClass : BaseClass
{
[MethodImpl(MethodImplOptions.NoInlining)]
public override void VirtualMethod() {}
}
class Program
{
const int Iters = 100_000_000;
static readonly long emptyLoopTime = MeasureEmptyLoop();
static void Main(string[] args)
{
var obj = new DerivedClass();
// Different ways to prejit methods
switch (args[0])
{
case "call":
obj.Method();
break;
case "virtual":
obj.VirtualMethod();
break;
case "interface":
obj.Method();
break;
case "delegate":
obj.Method();
//RuntimeHelpers.PrepareDelegate((Action)obj.Method); // Does nothing in .NET Core 2.0
break;
}
MeasureIt(obj, args[0]);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void MeasureIt(DerivedClass obj, string kind)
{
switch (kind)
{
case "call":
WriteLine($"{nameof(MeasureCall)}: {MillionTimesPerSec(MeasureCall(obj)):N3}");
break;
case "virtual":
WriteLine($"{nameof(MeasureVirtualCall)}: {MillionTimesPerSec(MeasureVirtualCall(obj)):N3}");
break;
case "interface":
WriteLine($"{nameof(MeasureInterfaceCall)}: {MillionTimesPerSec(MeasureInterfaceCall(obj)):N3}");
break;
case "delegate":
double res = MillionTimesPerSec(MeasureDelegateCall(obj.Method));
WriteLine($"{nameof(MeasureDelegateCall)}: {res:N3}");
break;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static long MeasureDelegateCall(Action action)
{
long start = Stopwatch.GetTimestamp();
for (int i = Iters; i != 0; --i)
{
action();
}
return Stopwatch.GetTimestamp() - start;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static long MeasureVirtualCall(BaseClass obj)
{
long start = Stopwatch.GetTimestamp();
for (int i = Iters; i != 0; --i)
{
obj.VirtualMethod();
}
return Stopwatch.GetTimestamp() - start;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static long MeasureCall(BaseClass obj)
{
long start = Stopwatch.GetTimestamp();
for (int i = Iters; i != 0; --i)
{
obj.Method();
}
return Stopwatch.GetTimestamp() - start;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static long MeasureInterfaceCall(IInter obj)
{
long start = Stopwatch.GetTimestamp();
for (int i = Iters; i != 0; --i)
{
obj.Method();
}
return Stopwatch.GetTimestamp() - start;
}
private static double MillionTimesPerSec(long ticks)
{
return Iters / ((ticks - emptyLoopTime) / (double)Stopwatch.Frequency) / 1_000_000;
}
private static long MeasureEmptyLoop()
{
long start = Stopwatch.GetTimestamp();
for (int i = Iters; i != 0; --i) { }
return Stopwatch.GetTimestamp() - start;
}
}
}
@xoofx

This comment has been minimized.

Copy link

@xoofx xoofx commented Feb 22, 2018

I have updated your version, removing the NoInlining on the methods, adding a constraint to store a result (to avoid simplification of the loop), adding a parameter (to match the original test i was doing), adding multiple calls into the loop to mitigate the cost of the loop

The results are still +20% faster for a virtual call vs a delegate call.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

using static System.Console;

namespace TestPerf
{
    interface IInter
    {
        int Method(int i);
    }

    class BaseClass : IInter
    {
        public int Method(int i)
        {
            return i + 1;

        }

        public virtual int VirtualMethod(int i)
        {
            return i + 1;
        }
    }

    class DerivedClass : BaseClass
    {
        public override int VirtualMethod(int i)
        {
            return i + 1;
        }
    }

    class Program
    {
        const int Iters = 500_000_000;
        static readonly long emptyLoopTime = MeasureEmptyLoop();
        private static int result = 0;

        static void Main(string[] args)
        {
            var obj = new DerivedClass();
            // Different ways to prejit methods
            switch (args[0])
            {
                case "call":
                    obj.Method(1);
                    break;

                case "virtual":
                    obj.VirtualMethod(1);
                    break;

                case "interface":
                    obj.Method(1);
                    break;

                case "delegate":
                    obj.Method(1);
                    //RuntimeHelpers.PrepareDelegate((Action)obj.Method); // Does nothing in .NET Core 2.0
                    break;
            }

            MeasureIt(obj, args[0]);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void MeasureIt(DerivedClass obj, string kind)
        {
            switch (kind)
            {
                case "call":
                    WriteLine($"{nameof(MeasureCall)}: {MillionTimesPerSec(MeasureCall(obj)):N3}");
                    break;

                case "virtual":
                    WriteLine($"{nameof(MeasureVirtualCall)}: {MillionTimesPerSec(MeasureVirtualCall(obj)):N3}");
                    break;

                case "interface":
                    WriteLine($"{nameof(MeasureInterfaceCall)}: {MillionTimesPerSec(MeasureInterfaceCall(obj)):N3}");
                    break;

                case "delegate":
                    double res = MillionTimesPerSec(MeasureDelegateCall(i => i + 1));
                    WriteLine($"{nameof(MeasureDelegateCall)}: {res:N3}");

                    break;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureDelegateCall(Func<int, int> action)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);

                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureVirtualCall(BaseClass obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);

                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureCall(BaseClass obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                obj.Method(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureInterfaceCall(IInter obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                obj.Method(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        private static double MillionTimesPerSec(long ticks)
        {
            return Iters / ((ticks - emptyLoopTime) / (double)Stopwatch.Frequency) / 1_000_000;
        }

        private static long MeasureEmptyLoop()
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i) { }

            return Stopwatch.GetTimestamp() - start;
        }
    }
}
@omariom

This comment has been minimized.

Copy link
Owner Author

@omariom omariom commented Feb 22, 2018

Thanks Alexandre!
Removing NoInline makes it "uncooked" again. Uncomment RuntimeHelpers.PrepareDelegate line then. And pass obj.Method to MeasureDelegateCall

@xoofx

This comment has been minimized.

Copy link

@xoofx xoofx commented Feb 23, 2018

Preparing won't change anything, because the time passed in the method is a several order of magnitudes bigger than JIT time (5s vs <1ms), so even If you add:

                    Func<int, int> del = i => i + 1;
                    RuntimeHelpers.PrepareDelegate(del);
                    double res = MillionTimesPerSec(MeasureDelegateCall(del));

You won't see any changes.

@omariom

This comment has been minimized.

Copy link
Owner Author

@omariom omariom commented Feb 23, 2018

@xoofx

It is not about jit time.
If a method is jitted before a delegate to it is created the delegate will get direct address of the jitted method.
Otherwise the delegate will get address of a jit thunk and calling via that delegate instance will also (and always) include jumping though the thunk.

Your code actually shows that delegates can be slightly faster than virtual dispatch :)

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

using static System.Console;

namespace TestPerf
{
    interface IInter
    {
        int Method(int i);
    }

    class BaseClass : IInter
    {
        public int Method(int i)
        {
            return i + 1;

        }

        public virtual int VirtualMethod(int i)
        {
            return i + 1;
        }
    }

    class DerivedClass : BaseClass
    {
        public override int VirtualMethod(int i)
        {
            return i + 1;
        }
    }

    class Program
    {
        const int Iters = 500_000_000;
        static readonly long emptyLoopTime = MeasureEmptyLoop();
        private static int result = 0;

        static void Main(string[] args)
        {
            var obj = new DerivedClass();
            // Different ways to prejit methods
            switch (args[0])
            {
                case "call":
                    obj.Method(1);
                    break;

                case "virtual":
                    obj.VirtualMethod(1);
                    break;

                case "interface":
                    obj.Method(1);
                    break;

                case "delegate":
                    //obj.Method(1);
                    RuntimeHelpers.PrepareDelegate((Func<int, int>)obj.Method); // Does nothing in .NET Core 2.0
                    break;
            }

            MeasureIt(obj, args[0]);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void MeasureIt(DerivedClass obj, string kind)
        {
            switch (kind)
            {
                case "call":
                    WriteLine($"{nameof(MeasureCall)}: {MillionTimesPerSec(MeasureCall(obj)):N3}");
                    break;

                case "virtual":
                    WriteLine($"{nameof(MeasureVirtualCall)}: {MillionTimesPerSec(MeasureVirtualCall(obj)):N3}");
                    break;

                case "interface":
                    WriteLine($"{nameof(MeasureInterfaceCall)}: {MillionTimesPerSec(MeasureInterfaceCall(obj)):N3}");
                    break;

                case "delegate":
                    double res = MillionTimesPerSec(MeasureDelegateCall(obj.Method));
                    WriteLine($"{nameof(MeasureDelegateCall)}: {res:N3}");

                    break;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureDelegateCall(Func<int, int> action)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);

                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
                result += action(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureVirtualCall(BaseClass obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);

                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
                result += obj.VirtualMethod(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureCall(BaseClass obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                obj.Method(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static long MeasureInterfaceCall(IInter obj)
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i)
            {
                obj.Method(i);
            }

            return Stopwatch.GetTimestamp() - start;
        }

        private static double MillionTimesPerSec(long ticks)
        {
            return Iters / ((ticks - emptyLoopTime) / (double)Stopwatch.Frequency) / 1_000_000;
        }

        private static long MeasureEmptyLoop()
        {
            long start = Stopwatch.GetTimestamp();

            for (int i = Iters; i != 0; --i) { }

            return Stopwatch.GetTimestamp() - start;
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment