Skip to content

Instantly share code, notes, and snippets.

@TheXenocide
Last active November 7, 2022 18:28
Show Gist options
  • Save TheXenocide/8c34dc38a445816fd55faecb4df9e6ba to your computer and use it in GitHub Desktop.
Save TheXenocide/8c34dc38a445816fd55faecb4df9e6ba to your computer and use it in GitHub Desktop.
Prototype LINQPad script for optimized injection into fields without relying on reflection. Uses similar strategies as serializers to reduce dynamic instance field assignment overhead.
void Main()
{
const int runCount = 1000000;
// GetUninitializedObject is a special instantiation method primarily used by serializers to skip calling constructors
// It creates a completely uninitialized instance of a managed type (all 0s/nulls/etc. no constructor/initialization logic called)
//var emptyInstance = FormatterServices.GetUninitializedObject(typeof(TestClass));
//emptyInstance.Dump("Empty");
//FancyReinjector.Reinject(emptyInstance);
//emptyInstance.Dump("Same Instance");
SimpleReinjector.NoOp();
ReinjectionHelper.Reinjector = new SimpleReinjector();
// var emptyInstance = FormatterServices.GetUninitializedObject(typeof(TestClass));
//
// emptyInstance.Dump("Empty");
// ReinjectionHelper.Reinject(emptyInstance);
// emptyInstance.Dump("Same Instance");
RunTest(runCount, nameof(SimpleReinjector));
ModeratelyComplexReinjector.NoOp();
ReinjectionHelper.Reinjector = new ModeratelyComplexReinjector();
// var emptyInstance = FormatterServices.GetUninitializedObject(typeof(TestClass));
//
// emptyInstance.Dump("Empty");
// ReinjectionHelper.Reinject(emptyInstance);
// emptyInstance.Dump("Same Instance");
RunTest(runCount, nameof(ModeratelyComplexReinjector));
FancyReinjector.NoOp();
ReinjectionHelper.Reinjector = new FancyReinjector();
// emptyInstance = FormatterServices.GetUninitializedObject(typeof(TestClass));
//
// emptyInstance.Dump("Empty");
// ReinjectionHelper.Reinject(emptyInstance);
// emptyInstance.Dump("Same Instance");
RunTest(runCount, nameof(FancyReinjector));
}
static void RunTest(int instanceCount, string resultsLabel)
{
var sw = Stopwatch.StartNew();
for (int x = 0; x < instanceCount; x++)
{
var emptyInstance = FormatterServices.GetUninitializedObject(typeof(TestClass));
ReinjectionHelper.Reinject(emptyInstance);
}
sw.Stop();
sw.ElapsedMilliseconds.Dump(resultsLabel);
}
public static class ReinjectionHelper
{
public static IReinjector Reinjector { get; set; }
public static void Reinject<T>(T instance) where T : class
{
Reinjector.Reinject(instance);
}
}
public interface IReinjector
{
public void Reinject<T>(T instance) where T : class;
}
public static class MockServiceProvider
{
// Mock DI Service Provider
private static readonly Random rnd = new Random();
public static TService GetRequiredService<TService>()
{
if ("Testing" is TService stringRet) return stringRet;
if (rnd.Next() is TService intRet) return intRet;
if (new RandomIntWrapper { JustADemoValue = rnd.Next() } is TService wrapperRet) return wrapperRet;
return default(TService);
}
}
public class FancyReinjector : IReinjector
{
public void Reinject<T>(T instance)
where T : class
{
// TODO: refine type inheritance scanning
var reinjectionType = instance.GetType();
while (reinjectionType != typeof(object))
{
if (reinjectorMap.TryGetValue(reinjectionType, out Action<object>[] fieldReinjectors))
{
foreach (var fieldReinjector in fieldReinjectors)
{
fieldReinjector(instance);
}
}
reinjectionType = reinjectionType.BaseType;
}
}
// this is where everything gets really fancy
static Dictionary<Type, Action<object>[]> reinjectorMap;
public static void NoOp() {} // force initialization to exclude from instrumentation results
static FancyReinjector()
{
// this maps a type to an array of action delegates to invoke against an instance being reinjected.
// Each delegate reassigns a different field. The delegates are used to avoid reflection overhead costs
// on each field assignment. Instead a single reflection cost is paid up-front to build the delegate
// which executes much faster than using reflection to assign the value of the field.
reinjectorMap = new Dictionary<Type, Action<object>[]>();
// Simplified for prototype testing purposes. Normally this would be some sort of assembly scan at startup, etc.
var allTypes = new Type[] { typeof(TestClass) };
foreach (var type in allTypes)
{
var fieldInjectors = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) // TODO: DeclaredOnly? have to work out how the inheritence part works
.Where(f => f.GetCustomAttribute<ReinjectOnDeserializationAttribute>() != null)
.Select(f => CreateFieldReinjector(type, f))
.ToArray();
reinjectorMap.Add(type, fieldInjectors);
}
}
static Action<object> CreateFieldReinjector(Type targetInstanceType, FieldInfo field)
{
// this gets a generic method definition (a method that has generic type parameters that still aren't filled out, e.g. Method<T, F>)
var genericMethodDefinition = typeof(FancyReinjector).GetMethod(nameof(WrappedFieldReinjector), BindingFlags.NonPublic | BindingFlags.Static);
// this gets a concrete (executable) method with the generic type parameters filled out with specific types
var typedWrapperMethod = genericMethodDefinition.MakeGenericMethod(new Type[] { targetInstanceType, field.FieldType });
// this uses reflection to dynamically invoke the wrapper method which has a cost for using reflection, but we only invoke
// that one time during startup. The delegate it returns can be invoked over and over on seperate instances with almost no
// overhead.
Action<object> fieldReinjector = (Action<object>)typedWrapperMethod.Invoke(null, new object[] { field });
return fieldReinjector;
}
static Action<object> WrappedFieldReinjector<TInstanceType, TFieldType>(FieldInfo field)
where TInstanceType : class
{
// this creates a new delegate with the field definition and generic types wrapped in its closure so that
// future callers of this delegate don't need to know the FieldInfo or pass in the generic types, only
// an object (targetInstance) parameter, which it will pass along to the ReinjectField method, along with the
// enclosed field definition and generic typers.
Action<object> ret = (targetInstance) => ReinjectField<TInstanceType, TFieldType>(targetInstance, field);
return ret;
}
static void ReinjectField<TInstanceType, TFieldType>(object instance, FieldInfo field)
where TInstanceType : class
{
// this gets a reference (like a managed pointer) to field inside the instance
ref TFieldType fieldReference = ref GetClassFieldReference<TInstanceType, TFieldType>(instance, field);
// this assigns the value of the field *inside the instance* to whatever value is returned from GetRequiredService
fieldReference = MockServiceProvider.GetRequiredService<TFieldType>();
}
// helpful reference material:
// https://stackoverflow.com/questions/30817924/obtain-non-explicit-field-offset/56512720#56512720
// https://stackoverflow.com/questions/16073091/is-there-a-way-to-create-a-delegate-to-get-and-set-values-for-a-fieldinfo
// https://stackoverflow.com/questions/17130382/understanding-garbage-collection-in-net/17131389#17131389
// https://codeblog.jonskeet.uk/2008/08/09/making-reflection-fly-and-exploring-delegates/
// https://devblogs.microsoft.com/premier-developer/managed-object-internals-part-4-fields-layout/
static unsafe ref TFieldType GetClassFieldReference<TInstanceType, TFieldType>(object instance, FieldInfo field)
where TInstanceType : class // to protect against accidentially using this method on a struct, which would not work as expected
{
// these conditions belong in an #if DEBUG in the real codebase for performance purposes
if (!field.DeclaringType.IsAssignableFrom(typeof(TInstanceType)))
throw new ArgumentException($"{field.Name} from {field.DeclaringType.FullName} is not a field of {typeof(TInstanceType).FullName}.", nameof(field));
if (!typeof(TInstanceType).IsAssignableFrom(instance.GetType()))
throw new ArgumentException($"{instance.GetType().FullName} is not a subclass of {typeof(TInstanceType).FullName}", nameof(instance));
// "casts" the object to a an IntPtr that points to the object
var objectPointer = Unsafe.As<Object, IntPtr>(ref instance);
// increment objectPointer + object header size? + field distance from "start" of object memory to get to the field's address
objectPointer += IntPtr.Size + GetFieldOffset(field.FieldHandle);
// "casts" the pointer to a reference local (a sort of managed pointer)
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/jump-statements#ref-returns
// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/declarations#ref-locals
return ref Unsafe.AsRef<TFieldType>(objectPointer.ToPointer());
}
// calculates the distance between a pointer to the beginning of an object in memory and the field specified
// within that object's allocated memory
private static int GetFieldOffset(FieldInfo fi) => GetFieldOffset(fi.FieldHandle);
private static int GetFieldOffset(RuntimeFieldHandle h) => Marshal.ReadInt32(h.Value + (4 + IntPtr.Size)) & 0xFFFFFF;
}
public class SimpleReinjector : IReinjector
{
private static readonly MethodInfo getRequiredServiceDefinition = typeof(MockServiceProvider).GetMethod("GetRequiredService", BindingFlags.Static | BindingFlags.Public);
public static void NoOp() {}
public void Reinject<T>(T instance) where T : class
{
foreach (var field in instance.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (field.GetCustomAttribute<ReinjectOnDeserializationAttribute>() != null)
{
var service = getRequiredServiceDefinition.MakeGenericMethod(field.FieldType).Invoke(null, null);
field.SetValue(instance, service);
}
}
}
}
public class ModeratelyComplexReinjector : IReinjector
{
public void Reinject<T>(T instance)
where T : class
{
// TODO: refine type inheritance scanning
var reinjectionType = instance.GetType();
while (reinjectionType != typeof(object))
{
if (reinjectorMap.TryGetValue(reinjectionType, out Action<object>[] fieldReinjectors))
{
foreach (var fieldReinjector in fieldReinjectors)
{
fieldReinjector(instance);
}
}
reinjectionType = reinjectionType.BaseType;
}
}
// this is where everything gets really fancy
static Dictionary<Type, Action<object>[]> reinjectorMap;
public static void NoOp() {} // force initialization to exclude from instrumentation results
static ModeratelyComplexReinjector()
{
// this maps a type to an array of action delegates to invoke against an instance being reinjected.
// Each delegate reassigns a different field. The delegates are used to avoid reflection overhead costs
// on each field assignment. Instead a single reflection cost is paid up-front to build the delegate
// which executes much faster than using reflection to assign the value of the field.
reinjectorMap = new Dictionary<Type, Action<object>[]>();
// Simplified for prototype testing purposes. Normally this would be some sort of assembly scan at startup, etc.
var allTypes = new Type[] { typeof(TestClass) };
foreach (var type in allTypes)
{
var fieldInjectors = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) // TODO: DeclaredOnly? have to work out how the inheritence part works
.Where(f => f.GetCustomAttribute<ReinjectOnDeserializationAttribute>() != null)
.Select(f => CreateFieldReinjector(type, f))
.ToArray();
reinjectorMap.Add(type, fieldInjectors);
}
}
static Action<object> CreateFieldReinjector(Type targetInstanceType, FieldInfo field)
{
// this gets a generic method definition (a method that has generic type parameters that still aren't filled out, e.g. Method<T, F>)
var genericMethodDefinition = typeof(FancyReinjector).GetMethod(nameof(WrappedFieldReinjector), BindingFlags.NonPublic | BindingFlags.Static);
// this gets a concrete (executable) method with the generic type parameters filled out with specific types
var typedWrapperMethod = genericMethodDefinition.MakeGenericMethod(new Type[] { targetInstanceType, field.FieldType });
// this uses reflection to dynamically invoke the wrapper method which has a cost for using reflection, but we only invoke
// that one time during startup. The delegate it returns can be invoked over and over on seperate instances with almost no
// overhead.
Action<object> fieldReinjector = (Action<object>)typedWrapperMethod.Invoke(null, new object[] { field });
return fieldReinjector;
}
static Action<object> WrappedFieldReinjector<TInstanceType, TFieldType>(FieldInfo field)
where TInstanceType : class
{
// this creates a new delegate with the field definition and generic types wrapped in its closure so that
// future callers of this delegate don't need to know the FieldInfo or pass in the generic types, only
// an object (targetInstance) parameter, which it will pass along to the ReinjectField method, along with the
// enclosed field definition and generic typers.
Action<object> ret = (targetInstance) => ReinjectField<TInstanceType, TFieldType>(targetInstance, field);
return ret;
}
static void ReinjectField<TInstanceType, TFieldType>(object instance, FieldInfo field)
where TInstanceType : class
{
// this assigns the value of the field *inside the instance* to whatever value is returned from GetRequiredService
var service = MockServiceProvider.GetRequiredService<TFieldType>();
field.SetValue(instance, service);
}
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class ReinjectOnDeserializationAttribute : Attribute
{
}
public class RandomIntWrapper
{
public int JustADemoValue { get; set; }
}
public class TestClass
{
public TestClass(string sampleProp)
{
SampleProp = sampleProp;
}
[field: ReinjectOnDeserialization]
public string SampleProp { get; }
public RandomIntWrapper IntentionallyNull { get; set; }
[field: ReinjectOnDeserialization]
public RandomIntWrapper DemoOnlyAfterReinject { get; }
[ReinjectOnDeserialization]
private int sampleSimpleField;
object ToDump() => new
{
SampleProp,
IntentionallyNull,
DemoOnlyAfterReinject,
sampleSimpleField
};
}
@TheXenocide
Copy link
Author

It's worth noting that there appears to be a negligible performance difference between the FancyReinjector and ModeratelyComplexReinjector with either outperforming the other from run to run, but both being in relatively the same window. The cached delegate behavior shared between them definitely shows a lot of performance improvement over SimpleReinjector but the ref local behavior doesn't seem to have a major impact over FieldInfo.SetValue in these tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment