// Copyright (c) 2022 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Diagnostics.CodeAnalysis; using System.Reflection.Emit; using ConstructorInfoCache = PerformanceFish.Cache.ByReference, System.Reflection.ConstructorInfo>; using FieldGetters = PerformanceFish.Cache.ByReference; using FieldInfoCache = PerformanceFish.Cache.ByReference; using FieldSetters = PerformanceFish.Cache.ByReference; using MethodInfoCache = PerformanceFish.Cache.ByReference; using MethodInfoWithTypesAndFlagsCache = PerformanceFish.Cache.ByReference, System.Reflection.MethodInfo>; using MethodInfoWithTypesCache = PerformanceFish.Cache.ByReference, System.Reflection.MethodInfo>; using MethodInvokers = PerformanceFish.Cache.ByReference; using PropertyInfoCache = PerformanceFish.Cache.ByReference; namespace PerformanceFish.System; public class ReflectionCaching : ClassWithFishPatches { public static void Initialize() // necessary to prevent recursion between patches and function pointer creation { try { ConstructorInfoCache.Initialize(); FieldGetters.Initialize(); FieldInfoCache.Initialize(); FieldSetters.Initialize(); MethodInfoCache.Initialize(); MethodInfoWithTypesAndFlagsCache.Initialize(); MethodInfoWithTypesCache.Initialize(); MethodInvokers.Initialize(); PropertyInfoCache.Initialize(); } catch (Exception ex) { Log.Error($"Performance Fish encountered an exception while trying to initialize its ReflectionCaching:\n{ex}"); } } public struct StateAndFlags { public bool State; public BindingFlags Flags; } public class TypePatches { public class GetField_Patch : FishPatch { public override string Description => "Caches GetField lookups"; public override MethodBase TargetMethodInfo => Reflection.MethodInfo("mscorlib", "System.RuntimeType", nameof(Type.GetField), new[] { typeof(string), typeof(BindingFlags) }); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, BindingFlags bindingAttr, ref FieldInfo? __result, out StateAndFlags __state) { var key = new FieldInfoCache(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr)); __result = FieldInfoCache.Get.TryGetValue(ref key); if (__result is null) { __state = new() { State = true, Flags = bindingAttr }; return true; } else { __state = new() { State = false, Flags = bindingAttr }; return false; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, string name, FieldInfo __result, StateAndFlags __state) { if (!__state.State || __result is null) return; FieldInfoCache.Get[new(__instance.TypeHandle, __state.Flags, ToLowerIfNeededForBindingFlags(name, __state.Flags))] = __result; } } public class GetMethodWithFlags_Patch : FishPatch { public override string Description => "Caches GetMethod lookups"; public override Expression TargetMethod => () => default(Type)!.GetMethod(null, default(BindingFlags)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, BindingFlags bindingAttr, ref MethodInfo? __result, out bool __state) { var key = new MethodInfoCache(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr)); __result = MethodInfoCache.Get.TryGetValue(ref key); return __state = __result is null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, string name, BindingFlags bindingAttr, MethodInfo __result, bool __state) { if (!__state || __result is null) return; MethodInfoCache.Get[new(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr))] = __result; } } public class GetMethodWithTypes_Patch : FishPatch { public override string Description => "Caches GetMethod lookups"; public override Expression TargetMethod => () => default(Type)!.GetMethod(null, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, Type[] types, ref MethodInfo? __result, out bool __state) { var key = new MethodInfoWithTypesCache(__instance.TypeHandle, name, new(types)); __result = MethodInfoWithTypesCache.Get.TryGetValue(ref key); return __state = __result is null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, string name, Type[] types, MethodInfo __result, bool __state) { if (!__state || __result is null) return; MethodInfoWithTypesCache.Get[new(__instance.TypeHandle, name, new(types))] = __result; } } public class GetMethodWithFlagsAndTypes_Patch : FishPatch { public override string Description => "Caches GetMethod lookups"; public override Expression TargetMethod => () => default(Type)!.GetMethod(null, default, null, null, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers, ref MethodInfo? __result, out bool __state) { if (binder != null || modifiers is { Length: > 0 }) { __state = false; return true; } var key = new MethodInfoWithTypesAndFlagsCache(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr), new(types)); __result = MethodInfoWithTypesAndFlagsCache.Get.TryGetValue(ref key); return __state = __result is null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, string name, Type[] types, BindingFlags bindingAttr, MethodInfo __result, bool __state) { if (!__state || __result is null) return; MethodInfoWithTypesAndFlagsCache.Get[new(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr), new(types))] = __result; } } public class GetConstructor_Patch : FishPatch { public override string Description => "Caches GetConstructor lookups"; public override Expression TargetMethod => () => default(Type)!.GetConstructor(default, null, null, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers, ref ConstructorInfo? __result, out bool __state) { if (binder != null || modifiers is { Length: > 0 }) { __state = false; return true; } var key = new ConstructorInfoCache(__instance.TypeHandle, bindingAttr, new(types)); __result = ConstructorInfoCache.Get.TryGetValue(ref key); return __state = __result is null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, BindingFlags bindingAttr, Type[] types, ConstructorInfo __result, bool __state) { if (!__state || __result is null) return; ConstructorInfoCache.Get[new(__instance.TypeHandle, bindingAttr, new(types))] = __result; } } public class GetProperty_Patch : FishPatch { public override string Description => "Caches GetProperty lookups"; public override Expression TargetMethod => () => default(Type)!.GetProperty(null, default(BindingFlags)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(Type __instance, string name, BindingFlags bindingAttr, ref PropertyInfo? __result, out bool __state) { var key = new PropertyInfoCache(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr)); __result = PropertyInfoCache.Get.TryGetValue(ref key); return __state = __result is null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Postfix(Type __instance, string name, BindingFlags bindingAttr, PropertyInfo __result, bool __state) { if (!__state || __result is null) return; PropertyInfoCache.Get[new(__instance.TypeHandle, bindingAttr, ToLowerIfNeededForBindingFlags(name, bindingAttr))] = __result; } } // probably never actually needed [MethodImpl(MethodImplOptions.AggressiveInlining)] private static string ToLowerIfNeededForBindingFlags(string name, BindingFlags bindingAttr) => (bindingAttr & BindingFlags.IgnoreCase) != 0 ? name.ToLowerInvariant() : name; } public struct ByValueComparableArray : IEquatable> where T : class { public T[] Array; [MethodImpl(MethodImplOptions.AggressiveInlining)] public ByValueComparableArray(T[] array) { Guard.IsNotNull(array); Array = array; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Equals(ByValueComparableArray other) { var length = Array.Length; if (length != other.Array.Length) return false; for (var i = 0; i < length; i++) { if (!Array[i].Equals(other.Array[i])) return false; } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override bool Equals(object obj) => obj is ByValueComparableArray array && Equals(array); [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int GetHashCode() => HashCode.Combine(Array.Length, Array.Length > 0 && Array[0] is { } item ? item.GetHashCode() : 0); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator ==(ByValueComparableArray left, ByValueComparableArray right) => left.Equals(right); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool operator !=(ByValueComparableArray left, ByValueComparableArray right) => !(left == right); } #if eee == false public class FieldInfoPatches { public class GetValue_Patch : FishPatch { public override string Description => "Optimizes the GetValue method by invoking it through specialized cached delegates"; //public GetValue_Patch() //{ // var types = AllSubclassesNonAbstract(typeof(FieldInfo)) // .SelectMany(t // => DeclaredMethodOrNothing(t, nameof(FieldInfo.GetValue), new[] { typeof(object) })) // .Select(m => m.DeclaringType); // Log.Message(types.ToStringSafeEnumerable()); //} public override MethodBase TargetMethodInfo => Reflection.MethodInfo("mscorlib", "System.Reflection.MonoField", nameof(FieldInfo.GetValue)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(FieldInfo __instance, object obj, ref object __result) { ref var cache = ref FieldGetters.Get.TryGetReferenceUnsafe(__instance.FieldHandle); if (!Unsafe.IsNullRef(ref cache)) { if (cache.func != null || UpdateGetterCache(ref cache, __instance)) { __result = cache.func!(obj); return false; } return true; } else { return AddCache(__instance); } } private static bool AddCache(FieldInfo __instance) { FieldGetters.Get.Add(__instance.FieldHandle, new()); return true; } } public class SetValue_Patch : FishPatch { public override string Description => "Optimizes the SetValue method by invoking it through specialized cached delegates"; public override Expression TargetMethod => () => default(FieldInfo)!.SetValue(null, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(FieldInfo __instance, object obj, object value) { ref var cache = ref FieldSetters.Get.TryGetReferenceUnsafe(__instance.FieldHandle); if (!Unsafe.IsNullRef(ref cache)) { if (cache.action != null || UpdateSetterCache(ref cache, __instance)) { cache.action!(obj, value); return false; } return true; } else { return AddCache(__instance); } } private static bool AddCache(FieldInfo __instance) { FieldSetters.Get.Add(__instance.FieldHandle, new()); return true; } } public static bool UpdateGetterCache(ref FieldInfoGetterCache cache, FieldInfo instance) { cache.callCount++; if (cache.callCount == 300) cache.func = MakeGetterDelegate(instance); else if (cache.callCount > 300) return false; FieldGetters.Get[instance.FieldHandle] = cache; return cache.func != null; } public static bool UpdateSetterCache(ref FieldInfoSetterCache cache, FieldInfo instance) { cache.callCount++; if (cache.callCount == 300) cache.action = MakeSetterDelegate(instance); else if (cache.callCount > 300) return false; FieldSetters.Get[instance.FieldHandle] = cache; return cache.action != null; } private static FieldGetter? MakeGetterDelegate(FieldInfo info) { try { var dm = new DynamicMethod($"FieldGetter_{info.Name}", typeof(object), new[] { typeof(object) }, info.DeclaringType, true); var il = dm.GetILGenerator(); if (!info.IsStatic) { il.Emit(FishTranspiler.This); il.Emit(info.DeclaringType.IsValueType ? FishTranspiler.Unbox(info.DeclaringType) : FishTranspiler.Cast(info.DeclaringType)); } il.Emit( /*info.DeclaringType.IsEnum && info.DeclaringType.GetEnumUnderlyingType() is var enumType ? enumType == typeof(long) || enumType == typeof(ulong) ? FishTranspiler.Constant((long)info.GetValue(info.DeclaringType)) : FishTranspiler.Constant((int)info.GetValue(info.DeclaringType)) :*/ info.IsLiteral && !info.IsInitOnly ? FishTranspiler.Constant(info.GetRawConstantValue()) : FishTranspiler.Field(info)); if (info.FieldType.IsValueType) il.Emit(FishTranspiler.Box(info.FieldType)); il.Emit(FishTranspiler.Return); return (FieldGetter)dm.CreateDelegate(typeof(FieldGetter)); } catch (Exception ex) { Log.Error($"PerformanceFish failed to generate an optimized delegate for {GetFieldDescription(info)}. Reverting to default behaviour instead.\n{ex}"); return null; } } private static FieldSetter? MakeSetterDelegate(FieldInfo info) { try { var dm = new DynamicMethod($"FieldSetter_{info.Name}", typeof(void), new[] { typeof(object), typeof(object) }, info.DeclaringType, true); var il = dm.GetILGenerator(); if (!info.IsStatic) { il.Emit(FishTranspiler.This); il.Emit(info.DeclaringType.IsValueType ? FishTranspiler.Unbox(info.DeclaringType) : FishTranspiler.Cast(info.DeclaringType)); } il.Emit(FishTranspiler.Argument(1)); if (info.FieldType.IsValueType) { //il.Emit(FishTranspiler.UnboxAny(info.FieldType)); il.Emit(FishTranspiler.Call(typeof(ReflectionCaching), info.FieldType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely), new[] { typeof(object) }, new[] { info.FieldType.IsNullable() ? Nullable.GetUnderlyingType(info.FieldType) : info.FieldType })); } else { il.Emit(FishTranspiler.Cast(info.FieldType)); } il.Emit(FishTranspiler.StoreField(info)); il.Emit(FishTranspiler.Return); return (FieldSetter)dm.CreateDelegate(typeof(FieldSetter)); } catch (Exception ex) { Log.Error($"PerformanceFish failed to generate an optimized delegate for {GetFieldDescription(info)}. Reverting to default behaviour instead.\n{ex}"); return null; } } public struct FieldInfoGetterCache { public FieldGetter? func; public int callCount; } public struct FieldInfoSetterCache { public FieldSetter? action; public int callCount; } public delegate object FieldGetter(object obj); public delegate void FieldSetter(object obj, object value); } private static string GetFieldDescription(FieldInfo info) => $"{(info.IsPublic ? "public" : "private")} {(info.IsLiteral && !info.IsInitOnly ? "const " : info.IsStatic ? "static " : "")} {info.FieldType.FullDescription()} {info.DeclaringType.FullDescription()}:{info.Name}"; public class MethodBasePatches { public class Invoke_Patch : FishPatch { public override string Description => "Optimizes the Invoke method by invoking it through specialized cached delegates"; public override Expression TargetMethod => () => default(MethodBase)!.Invoke(null, null); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Prefix(MethodBase __instance, object obj, object[] parameters, ref object __result) { if (__instance is not MethodInfo method) return true; ref var cache = ref MethodInvokers.Get.TryGetReferenceUnsafe(method.MethodHandle); if (!Unsafe.IsNullRef(ref cache)) { if (cache.func != null || UpdateCache(ref cache, method)) { __result = cache.func!(obj, parameters); return false; } return true; } else { return AddCache(method); } } private static bool AddCache(MethodInfo method) { MethodInvokers.Get.Add(method.MethodHandle, new()); return true; } } public static bool UpdateCache(ref MethodInvokerCache cache, MethodInfo instance) { cache.callCount++; if (cache.callCount == 300) cache.func = MakeInvokeDelegate(instance); else if (cache.callCount > 300) return false; MethodInvokers.Get[instance.MethodHandle] = cache; return cache.func != null; } // HarmonyLib.MethodInvoker can't handle arrays with null values when used with ByRef value types. That aside this is near identical. public static MethodInvoker? MakeInvokeDelegate(MethodInfo info) { try { //DebugLog.Message($"Making delegate for {(info.IsStatic ? "static" : "")} {info.DeclaringType} {info} with arguments {info.GetParameters().ToStringSafeEnumerable()}"); var dm = new DynamicMethod($"MethodInvoker_{info.Name}", typeof(object), new[] { typeof(object), typeof(object[]) }, info.DeclaringType, true); var il = dm.GetILGenerator(); var parameters = info.GetParameters(); if (!info.IsStatic) { il.Emit(FishTranspiler.This); if (info.DeclaringType.IsValueType) il.Emit(FishTranspiler.UnboxAny(info.DeclaringType)); } for (var i = 0; i < parameters.Length; i++) { il.Emit(FishTranspiler.Argument(1)); il.Emit(FishTranspiler.Constant(i)); var parameterType = parameters[i].ParameterType; if (parameterType.IsByRef) { parameterType = parameterType.GetElementType(); il.Emit(FishTranspiler.LoadElementAddress()); il.Emit(parameterType.IsValueType ? FishTranspiler.Call(typeof(ReflectionCaching), nameof(UnboxRefSafely), new[] { typeof(object).MakeByRefType() }, new[] { parameterType.IsNullable() ? Nullable.GetUnderlyingType(parameterType) : parameterType }) : FishTranspiler.Call(typeof(ReflectionCaching), nameof(CastRefSafely), new[] { typeof(object).MakeByRefType() }, new[] { typeof(object), parameterType })); } else { il.Emit(FishTranspiler.LoadElement()); il.Emit(parameterType.IsValueType ? FishTranspiler.Call(typeof(ReflectionCaching), parameterType.IsNullable() ? nameof(UnboxNullableSafely) : nameof(UnboxSafely), new[] { typeof(object) }, new[] { parameterType.IsNullable() ? Nullable.GetUnderlyingType(parameterType) : parameterType }) : FishTranspiler.As(parameterType)); } } il.Emit(FishTranspiler.Call(info)); if (info.ReturnType == typeof(void)) il.Emit(FishTranspiler.Null); else if (info.ReturnType.IsValueType) il.Emit(FishTranspiler.Box(info.ReturnType)); il.Emit(FishTranspiler.Return); return (MethodInvoker)dm.CreateDelegate(typeof(MethodInvoker)); } catch (Exception ex) { Log.Error($"PerformanceFish failed to generate an optimized delegate for {info.FullDescription()}. Reverting to default behaviour instead.\n{ex}"); return null; } } public struct MethodInvokerCache { public MethodInvoker? func; public int callCount; } public delegate object MethodInvoker(object obj, object[] parameters); } #endif [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TTo CastRefSafely(ref TFrom from) => ref from is TTo or null ? ref Unsafe.As(ref from) : ref ThrowInvalidCastException(); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref TTo UnboxRefSafely(ref object from) where TTo : struct => ref from is TTo ? ref Unsafe.Unbox(from) : ref Unsafe.Unbox(from = from is null ? default : FisheryLib.Convert.To((IConvertible)from)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TTo UnboxSafely(object from) where TTo : struct => from is TTo to ? to // : from is IConvertible : FisheryLib.Convert.To((IConvertible)from); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TTo? UnboxNullableSafely(object from) where TTo : struct => from is TTo to ? to : from is null ? null // : from is IConvertible : FisheryLib.Convert.To((IConvertible)from); [DoesNotReturn] private static ref T ThrowInvalidCastException() => throw new InvalidCastException(); }