Skip to content

Instantly share code, notes, and snippets.

@Eideren
Last active October 25, 2024 15:07
Show Gist options
  • Save Eideren/b379bb5db698afb9c847138735e039a3 to your computer and use it in GitHub Desktop.
Save Eideren/b379bb5db698afb9c847138735e039a3 to your computer and use it in GitHub Desktop.
Eliding/inlining virtual calls in C#
Method ItemsCount Mean Error StdDev Min Max Rank
Elided 10 12.09 ns 0.043 ns 0.034 ns 12.050 ns 12.14 ns 2
Naive 10 10.04 ns 0.089 ns 0.079 ns 9.936 ns 10.20 ns 1
Elided 100 92.30 ns 0.786 ns 0.735 ns 90.977 ns 93.58 ns 3
Naive 100 153.22 ns 1.041 ns 0.923 ns 152.210 ns 155.50 ns 4
Elided 1000 808.97 ns 8.076 ns 7.555 ns 797.855 ns 823.80 ns 5
Naive 1000 1,550.42 ns 4.637 ns 4.110 ns 1,545.038 ns 1,559.88 ns 6
Elided 100000 169,402.00 ns 834.769 ns 740.001 ns 168,302.930 ns 170,912.40 ns 7
Naive 100000 497,252.59 ns 2,017.683 ns 1,575.274 ns 493,172.534 ns 498,835.08 ns 8

As you can see, the performance improvement can't be felt below 10 items, there's too much overhead before then.

using System;
using System.Collections.Generic;
public class ExampleUsage
{
public void Elided()
{
// Let's say you have a ton of objects inheriting from the same class or interface ...
var myObjects = new List<ICommonInterface>();
// You want to call a method over each one of them but are worried about the cost those virtual calls would introduce ...
foreach (var myAbstractObject in myObjects)
myAbstractObject.DoTheThing(10); // This would go through the VTable to find the right method to call, and also prevents any inlining
// You can instead break up each object by type ...
var eliders = new Dictionary<Type, Elider>();
foreach (var myAbstractObject in myObjects)
Elider.AddToHandlers(myAbstractObject, eliders); // ... and force the JIT to create one new type per type instance
// Then it's just a case of having a purpose built Elider for each case you may need,
// This sounds ripe for abstraction, sadly you can't really abstract as you would want because generalizing
// this prevents the JIT/PGO from inlining appropriately, leading to worse performance than the naive version.
Elider.DoTheThing(eliders);
}
}
public interface ICommonInterface
{
public int DoTheThing(int i);
public static int MyInt;
}
public sealed class ConcreteA : ICommonInterface { public int DoTheThing(int i) => ICommonInterface.MyInt += i + 1; }
public sealed class ConcreteB : ICommonInterface { public int DoTheThing(int i) => ICommonInterface.MyInt += i + 2; }
public sealed class ConcreteC : ICommonInterface { public int DoTheThing(int i) => ICommonInterface.MyInt += i + 3; }
public abstract class Elider
{
protected abstract void Add(ICommonInterface obj);
protected abstract bool Remove(ICommonInterface obj);
protected abstract void DoTheThing();
public static void AddToHandlers(ICommonInterface item, Dictionary<Type, Elider> handlers)
{
var concreteType = item.GetType();
if (handlers.TryGetValue(concreteType, out var batcher) == false)
{
var gen = typeof(Handler<>).MakeGenericType(concreteType); // Create one specific Handler<> for this concrete type
batcher = (Elider)Activator.CreateInstance(gen)!; // Create an instance of this specialized type to pass our items to it
handlers.Add(concreteType, batcher);
}
batcher.Add(item);
}
public static bool RemoveFromHandlers(ICommonInterface item, Dictionary<Type, Elider> handlers)
{
return handlers.TryGetValue(item.GetType(), out var handler) && handler.Remove(item);
}
public static void DoTheThing(Dictionary<Type, Elider> handlers)
{
foreach (var kvp in handlers)
kvp.Value.DoTheThing();
}
private sealed class Handler<T> : Elider where T : ICommonInterface // This class get specialized to a concrete type
{
private List<T> _abstraction = [];
protected override void Add(ICommonInterface obj) => _abstraction.Add((T)obj);
protected override bool Remove(ICommonInterface obj) => _abstraction.Remove((T)obj);
protected override void DoTheThing()
{
foreach (var abstraction in _abstraction)
abstraction.DoTheThing(10);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment