FastExpressionCompiler
FastExpressionCompiler copied to clipboard
Hack DynamicMethod
- Hack its construction
- Hack CreateDelegate in order to reuse the method again.
I've actually done some experiments with this before. By re-using DynamicMethod and IlGenerators I could reduce (on mono) allocations per delegate creation by 12. Performance wasn't directly impacted that much but since there's a lot of contention for the memory allocator (at least on the version of Mono inside Unity3d) I got some pretty big throughput increases while multi-threading compilation of many delegates.
@weichx Wow, how did you do that, can you share?
At the moment, the most promising part for me is to reuse byte codes byte[]
array here:
internal void EnsureCapacity(int size)
{
if (m_length + size >= m_ILStream.Length)
IncreaseCapacity(size);
}
private void IncreaseCapacity(int size)
{
byte[] temp = new byte[Math.Max(m_ILStream.Length * 2, m_length + size)]; // todo: @perf how to reuse existing ILStream here
Array.Copy(m_ILStream, temp, m_ILStream.Length);
m_ILStream = temp;
}
That was just a little prototype so I unfortunately don't have the code anymore but the gist of it was directly calling the internal C# runtime methods via reflection (or compiled expression tree). This let me skip all the api overhead that the .NET library introduces.
// this is a method defined in DynamicMethod.cs in the runtime library
// You can find it via reflection.
// I called this via reflection after setting all the properties on `m`
// which was reused across compilations
private static extern void create_dynamic_method (DynamicMethod m);
All of the C# -> C runtime methods have static extern
C# methods that are marked private or internal. Its just a matter of finding them and calling them yourself. I did the same thing for finding fields/properties/methods via reflection without allocating. The code below won't compile for you since it uses some of my utilities but hopefully it illustrates the idea.
public static unsafe class MonoUtil {
private static Action<IntPtr> s_DisposePtrFn;
private static Func<MethodInfo, ParameterInfo[]> s_GetParametersFn;
private static Func<Type, IntPtr, int, BindingFlags, IntPtr> s_GetMethodsByNameFn;
private static Func<Type, RuntimeTypeHandle> s_CreateRuntimeTypeHandleFn;
private static Func<IntPtr, RuntimeTypeHandle, MethodBase> s_CreateMethodInfo;
private static Func<IntPtr, RuntimeTypeHandle, FieldInfo> s_CreateFieldInfo;
private static Func<IntPtr, RuntimeTypeHandle, PropertyInfo> s_CreatePropertyInfo;
private static Func<Type, IntPtr, int, BindingFlags, IntPtr> s_GetFieldsByNameFn;
private static Func<Type, IntPtr, int, BindingFlags, IntPtr> s_GetPropertiesByNameFn;
// mirrors mono definition
private enum MemberListType {
All,
CaseSensitive,
CaseInsensitive,
HandleToInfo
}
public static void Initialize() {
CreateUtilFunctions();
}
public static int GetFields(Type type, BindingFlags bindingFlags, IList<FieldInfo> result) {
return GetFields(type, default(FixedCharacterSpan), bindingFlags, result);
}
public static int GetFields(Type type, string fieldName, BindingFlags bindingFlags, IList<FieldInfo> result) {
if (string.IsNullOrEmpty(fieldName)) {
return GetFields(type, default(FixedCharacterSpan), bindingFlags, result);
}
fixed (char* cbuffer = fieldName) {
return GetFields(type, new FixedCharacterSpan(cbuffer, fieldName.Length), bindingFlags, result);
}
}
internal struct GPtrArray {
internal IntPtr* data;
internal int len;
}
internal static int GetFields(Type type, FixedCharacterSpan fieldName, BindingFlags bindingFlags, IList<FieldInfo> result) {
IntPtr ptr = default;
if (fieldName.size != 0) {
int utf8Size = (fieldName.size * 2) + 4; // buffer a bit extra just in case
byte* cbuffer = stackalloc byte[utf8Size];
CopyError error = UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, utf8Size, fieldName.ptr, fieldName.size);
if (error == CopyError.Truncation) {
cbuffer = TypedUnsafe.Malloc<byte>(2 * utf8Size, Allocator.Temp);
UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, 2 * utf8Size, fieldName.ptr, fieldName.size);
}
ptr = new IntPtr(cbuffer);
}
IntPtr nativeMethodHandle = GetFieldsByName(type, ptr, bindingFlags);
RuntimeTypeHandle runtimeTypeHandle = CreateRuntimeTypeHandle(type);
GPtrArray* ptrArray = (GPtrArray*)nativeMethodHandle;
for (int i = 0; i < ptrArray->len; i++) {
result.Add(s_CreateFieldInfo(ptrArray->data[i], runtimeTypeHandle));
}
int retn = ptrArray->len;
DisposeMonoArray(nativeMethodHandle);
return retn;
}
public static int GetProperties(Type type, BindingFlags bindingFlags, IList<PropertyInfo> result) {
return GetProperties(type, default(FixedCharacterSpan), bindingFlags, result);
}
public static int GetProperties(Type type, string propertyName, BindingFlags bindingFlags, IList<PropertyInfo> result) {
if (string.IsNullOrEmpty(propertyName)) {
return GetProperties(type, default(FixedCharacterSpan), bindingFlags, result);
}
fixed (char* cbuffer = propertyName) {
return GetProperties(type, new FixedCharacterSpan(cbuffer, propertyName.Length), bindingFlags, result);
}
}
internal static int GetProperties(Type type, FixedCharacterSpan propertyName, BindingFlags bindingFlags, IList<PropertyInfo> result) {
IntPtr ptr = default;
if (propertyName.size != 0) {
int utf8Size = (propertyName.size * 2) + 4; // buffer a bit extra just in case
byte* cbuffer = stackalloc byte[utf8Size];
CopyError error = UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, utf8Size, propertyName.ptr, propertyName.size);
if (error == CopyError.Truncation) {
cbuffer = TypedUnsafe.Malloc<byte>(2 * utf8Size, Allocator.Temp);
UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, 2 * utf8Size, propertyName.ptr, propertyName.size);
}
ptr = new IntPtr(cbuffer);
}
IntPtr nativeMethodHandle = GetPropertiesByName(type, ptr, bindingFlags);
RuntimeTypeHandle runtimeTypeHandle = CreateRuntimeTypeHandle(type);
GPtrArray* ptrArray = (GPtrArray*)nativeMethodHandle;
for (int i = 0; i < ptrArray->len; i++) {
result.Add(s_CreatePropertyInfo(ptrArray->data[i], runtimeTypeHandle));
}
int retn = ptrArray->len;
DisposeMonoArray(nativeMethodHandle);
return retn;
}
internal static int GetMethods(Type type, IList<MethodInfo> result, BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) {
return GetMethods(type, default(FixedCharacterSpan), result, bindingFlags);
}
public static int GetMethods(Type type, string methodName, IList<MethodInfo> result, BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) {
if (string.IsNullOrEmpty(methodName)) {
return GetMethods(type, default(FixedCharacterSpan), result, bindingFlags);
}
fixed (char* cbuffer = methodName) {
return GetMethods(type, new FixedCharacterSpan(cbuffer, methodName.Length), result, bindingFlags);
}
}
internal static int GetMethods(Type type, FixedCharacterSpan methodName, IList<MethodInfo> result, BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) {
IntPtr ptr = default;
if (methodName.size != 0) {
int utf8Size = (methodName.size * 2) + 4; // buffer a bit extra just in case
byte* cbuffer = stackalloc byte[utf8Size];
CopyError error = UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, utf8Size, methodName.ptr, methodName.size);
if (error == CopyError.Truncation) {
cbuffer = TypedUnsafe.Malloc<byte>(2 * utf8Size, Allocator.Temp);
UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, 2 * utf8Size, methodName.ptr, methodName.size);
}
ptr = new IntPtr(cbuffer);
}
IntPtr nativeMethodHandle = GetMethodsByName(type, ptr, bindingFlags);
RuntimeTypeHandle runtimeTypeHandle = CreateRuntimeTypeHandle(type);
GPtrArray* ptrArray = (GPtrArray*)nativeMethodHandle;
for (int i = 0; i < ptrArray->len; i++) {
result.Add((MethodInfo)s_CreateMethodInfo(ptrArray->data[i], runtimeTypeHandle));
}
int retn = ptrArray->len;
DisposeMonoArray(nativeMethodHandle);
return retn;
}
private static MethodInfo CreateRuntimeMethodHandle(IntPtr ptr, RuntimeTypeHandle typeHandle) {
return (MethodInfo)s_CreateMethodInfo(ptr, typeHandle);
}
private static void CreateUtilFunctions() {
{
Type monoMethodInfoType = typeof(MethodInfo).Assembly.GetType("System.Reflection.MonoMethodInfo");
MethodInfo getparams = monoMethodInfoType.GetMethod("GetParametersInfo", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
ParameterExpression methodInfoParam = Expression.Parameter(typeof(MethodInfo));
PropertyInfo methodHandle = typeof(MethodInfo).GetProperty(nameof(MethodInfo.MethodHandle));
PropertyInfo value = methodHandle.PropertyType.GetProperty(nameof(MethodInfo.MethodHandle.Value));
MemberExpression handleProp = Expression.Property(methodInfoParam, methodHandle);
MemberExpression ptrValue = Expression.Property(handleProp, value);
s_GetParametersFn = Expression.Lambda<Func<MethodInfo, ParameterInfo[]>>(Expression.Call(null, getparams, ptrValue, methodInfoParam), new[] { methodInfoParam }).Compile();
}
Type runtimeMethodInfoType = typeof(Action).Assembly.GetType("System.Reflection.RuntimeMethodInfo");
Type runtimePropertyInfoType = typeof(Action).Assembly.GetType("System.Reflection.RuntimePropertyInfo");
Type runtimePropertyHandleType = typeof(Action).Assembly.GetType("Mono.RuntimePropertyHandle");
ConstructorInfo runtimeMethodHandleCtor = typeof(RuntimeMethodHandle).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
ConstructorInfo runtimeFieldHandleCtor = typeof(RuntimeFieldHandle).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
ConstructorInfo runtimePropertyHandleCtor = runtimePropertyHandleType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
MethodInfo[] runtimeMethodInfoMethods = runtimeMethodInfoType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static);
MethodInfo[] fieldInfoMethods = typeof(FieldInfo).GetMethods(BindingFlags.Public | BindingFlags.Static);
MethodInfo[] runtimePropertyInfoMethods = runtimePropertyInfoType.GetMethods(BindingFlags.NonPublic | BindingFlags.Static);
MethodInfo methodInfoGetMethodFromHandle = null;
MethodInfo fieldInfoGetFieldFromHandle = null;
MethodInfo propertyInfoGetPropertyFromHandle = null;
for (int i = 0; i < runtimeMethodInfoMethods.Length; i++) {
if (runtimeMethodInfoMethods[i].Name == "GetMethodFromHandleNoGenericCheck" && GetImmutableParameters(runtimeMethodInfoMethods[i]).Length == 2) {
methodInfoGetMethodFromHandle = runtimeMethodInfoMethods[i];
break;
}
}
for (int i = 0; i < fieldInfoMethods.Length; i++) {
if (fieldInfoMethods[i].Name == "GetFieldFromHandle" && GetImmutableParameters(fieldInfoMethods[i]).Length == 2) {
fieldInfoGetFieldFromHandle = fieldInfoMethods[i];
break;
}
}
for (int i = 0; i < runtimePropertyInfoMethods.Length; i++) {
if (runtimePropertyInfoMethods[i].Name == "GetPropertyFromHandle" && GetImmutableParameters(runtimePropertyInfoMethods[i]).Length == 2) {
propertyInfoGetPropertyFromHandle = runtimePropertyInfoMethods[i];
break;
}
}
ParameterExpression[] parameters = new[] {
Expression.Parameter(typeof(IntPtr)),
Expression.Parameter(typeof(RuntimeTypeHandle)),
};
s_CreateMethodInfo = Expression.Lambda<Func<IntPtr, RuntimeTypeHandle, MethodBase>>(Expression.Call(null, methodInfoGetMethodFromHandle, Expression.New(runtimeMethodHandleCtor, parameters[0]), parameters[1]), parameters).Compile();
s_CreateFieldInfo = Expression.Lambda<Func<IntPtr, RuntimeTypeHandle, FieldInfo>>(Expression.Call(null, fieldInfoGetFieldFromHandle, Expression.New(runtimeFieldHandleCtor, parameters[0]), parameters[1]), parameters).Compile();
s_CreatePropertyInfo = Expression.Lambda<Func<IntPtr, RuntimeTypeHandle, PropertyInfo>>(Expression.Call(null, propertyInfoGetPropertyFromHandle, Expression.New(runtimePropertyHandleCtor, parameters[0]), parameters[1]), parameters).Compile();
Type safeGPtrArrayHandle = typeof(Action).Assembly.GetType("Mono.SafeGPtrArrayHandle");
ParameterExpression intPtrParam = Expression.Parameter(typeof(IntPtr));
ConstructorInfo safeHandleCtor = safeGPtrArrayHandle.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
// new SafeGPtrArrayHandle(ptr).Dispose(); this lets me invoke the dispose w/o dealing w/ pointers which I can't do via expr trees
s_DisposePtrFn = Expression.Lambda<Action<IntPtr>>(Expression.Call(
Expression.New(safeHandleCtor, intPtrParam),
safeGPtrArrayHandle.GetMethod("Dispose", BindingFlags.Instance | BindingFlags.Public)
), intPtrParam).Compile();
Type systemRuntimeType = typeof(Action).Assembly.GetType("System.RuntimeType");
ConstructorInfo rtHandleCtor = typeof(RuntimeTypeHandle).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { systemRuntimeType }, null);
ParameterExpression parameter = Expression.Parameter(typeof(Type), "type");
s_CreateRuntimeTypeHandleFn = Expression.Lambda<Func<Type, RuntimeTypeHandle>>(
Expression.New(rtHandleCtor, Expression.Convert(parameter, systemRuntimeType)),
parameter
).Compile();
MethodInfo getFields_native = systemRuntimeType.GetMethod("GetFields_native", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo getProperties_native = systemRuntimeType.GetMethod("GetPropertiesByName_native", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo getConstructors_native = systemRuntimeType.GetMethod("GetConstructors_native", BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo getMethodsByName_native = systemRuntimeType.GetMethod("GetMethodsByName_native", BindingFlags.NonPublic | BindingFlags.Instance);
Type memberListType = typeof(Action).Assembly.GetType("System.RuntimeType+MemberListType");
ParameterExpression typeParameter = Expression.Parameter(typeof(Type));
ParameterExpression nameParameter = Expression.Parameter(typeof(IntPtr));
ParameterExpression memberTypeListVal = Expression.Parameter(typeof(int));
ParameterExpression bindingFlags = Expression.Parameter(typeof(BindingFlags));
// ConstantExpression bindingFlags = Expression.Constant(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
ParameterExpression[] paramList3 = new[] { typeParameter, nameParameter, memberTypeListVal };
ParameterExpression[] paramList4 = new[] { typeParameter, nameParameter, memberTypeListVal, bindingFlags };
s_GetMethodsByNameFn = Expression.Lambda<Func<Type, IntPtr, int, BindingFlags, IntPtr>>(Expression.Call(
Expression.Convert(typeParameter, systemRuntimeType),
getMethodsByName_native,
nameParameter,
bindingFlags,
Expression.Convert(memberTypeListVal, memberListType)
), paramList4).Compile();
s_GetFieldsByNameFn = Expression.Lambda<Func<Type, IntPtr, int, BindingFlags, IntPtr>>(Expression.Call(
Expression.Convert(typeParameter, systemRuntimeType),
getFields_native,
nameParameter,
bindingFlags,
Expression.Convert(memberTypeListVal, memberListType)
), paramList4).Compile();
s_GetPropertiesByNameFn = Expression.Lambda<Func<Type, IntPtr, int, BindingFlags, IntPtr>>(Expression.Call(
Expression.Convert(typeParameter, systemRuntimeType),
getProperties_native,
nameParameter,
bindingFlags,
Expression.Convert(memberTypeListVal, memberListType)
), paramList4).Compile();
}
public static void DisposeMonoArray(IntPtr ptr) {
s_DisposePtrFn(ptr);
}
public static RuntimeTypeHandle CreateRuntimeTypeHandle(Type type) {
return s_CreateRuntimeTypeHandleFn(type);
}
private static IntPtr GetMethodsByName(Type type, IntPtr methodName, BindingFlags bindingFlags) {
return s_GetMethodsByNameFn(type, methodName, methodName.ToInt64() == 0 ? (int)MemberListType.All : (int)MemberListType.CaseSensitive, bindingFlags);
}
private static IntPtr GetFieldsByName(Type type, IntPtr fieldName, BindingFlags bindingFlags) {
return s_GetFieldsByNameFn(type, fieldName, fieldName.ToInt64() == 0 ? (int)MemberListType.All : (int)MemberListType.CaseSensitive, bindingFlags);
}
private static IntPtr GetPropertiesByName(Type type, IntPtr fieldName, BindingFlags bindingFlags) {
return s_GetPropertiesByNameFn(type, fieldName, fieldName.ToInt64() == 0 ? (int)MemberListType.All : (int)MemberListType.CaseSensitive, bindingFlags);
}
public static ParameterInfo[] GetImmutableParameters(MethodBase methodBase) {
if (methodBase is ConstructorInfo ctor) {
return GetImmutableParameters(ctor);
}
return GetImmutableParameters((MethodInfo)methodBase);
}
public static ParameterInfo[] GetImmutableParameters(MethodInfo methodInfo) {
// this proooobablly needs a lock or the caller needs to be locked
// this is calling directly into mono to get at the parameter list that it uses internally
// which prevents an array allocation & copy. Because we use their direct copy we CANNOT
// mutate that array or weird shit will start happening.
return s_GetParametersFn.Invoke(methodInfo);
}
internal static bool TryGetFieldByName(Type type, string fieldName, BindingFlags bindingFlags, out FieldInfo fieldInfo) {
fixed (char* cbuffer = fieldName) {
return TryGetFieldByName(type, new FixedCharacterSpan(cbuffer, fieldName.Length), bindingFlags, out fieldInfo);
}
}
internal static bool TryGetFieldByName(Type type, FixedCharacterSpan fieldName, BindingFlags bindingFlags, out FieldInfo fieldInfo) {
IntPtr ptr = default;
if (fieldName.size != 0) {
int utf8Size = (fieldName.size * 2) + 4; // buffer a bit extra just in case
byte* cbuffer = stackalloc byte[utf8Size];
CopyError error = UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, utf8Size, fieldName.ptr, fieldName.size);
if (error == CopyError.Truncation) {
cbuffer = TypedUnsafe.Malloc<byte>(2 * utf8Size, Allocator.Temp);
UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, 2 * utf8Size, fieldName.ptr, fieldName.size);
}
ptr = new IntPtr(cbuffer);
}
IntPtr nativeMethodHandle = GetFieldsByName(type, ptr, bindingFlags);
RuntimeTypeHandle runtimeTypeHandle = CreateRuntimeTypeHandle(type);
GPtrArray* ptrArray = (GPtrArray*)nativeMethodHandle;
if (ptrArray->len >= 1) {
// return first match, I don't know when there would ever be more than 1. Maybe w/ base types & shadowing?
fieldInfo = s_CreateFieldInfo(ptrArray->data[0], runtimeTypeHandle);
DisposeMonoArray(nativeMethodHandle);
return true;
}
fieldInfo = default;
DisposeMonoArray(nativeMethodHandle);
return false;
}
internal static bool TryGetPropertyByName(Type type, string propertyName, BindingFlags bindingFlags, out PropertyInfo propertyInfo) {
fixed (char* cbuffer = propertyName) {
return TryGetPropertyByName(type, new FixedCharacterSpan(cbuffer, propertyName.Length), bindingFlags, out propertyInfo);
}
}
internal static bool TryGetPropertyByName(Type type, FixedCharacterSpan propertyName, BindingFlags bindingFlags, out PropertyInfo propertyInfo) {
IntPtr ptr = default;
if (propertyName.size != 0) {
int utf8Size = (propertyName.size * 2) + 4; // buffer a bit extra just in case
byte* cbuffer = stackalloc byte[utf8Size];
CopyError error = UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, utf8Size, propertyName.ptr, propertyName.size);
if (error == CopyError.Truncation) {
cbuffer = TypedUnsafe.Malloc<byte>(2 * utf8Size, Allocator.Temp);
UTF8ArrayUnsafeUtility.Copy(cbuffer, out _, 2 * utf8Size, propertyName.ptr, propertyName.size);
}
ptr = new IntPtr(cbuffer);
}
IntPtr nativeMethodHandle = GetPropertiesByName(type, ptr, bindingFlags);
RuntimeTypeHandle runtimeTypeHandle = CreateRuntimeTypeHandle(type);
GPtrArray* ptrArray = (GPtrArray*)nativeMethodHandle;
if (ptrArray->len >= 1) {
// return first match, I don't know when there would ever be more than 1. Maybe w/ base types & shadowing?
propertyInfo = s_CreatePropertyInfo(ptrArray->data[0], runtimeTypeHandle);
DisposeMonoArray(nativeMethodHandle);
return true;
}
propertyInfo = default;
DisposeMonoArray(nativeMethodHandle);
return false;
}
// This will return enumValues and enumNames sorted by the values.
public static void GetEnumData(Type type, out string[] enumNames, out Array enumValues) {
FieldInfo[] flds = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
object[] values = new object[flds.Length];
string[] names = new string[flds.Length];
for (int i = 0; i < flds.Length; i++) {
names[i] = flds[i].Name;
values[i] = flds[i].GetRawConstantValue();
}
enumNames = names;
enumValues = values;
}
}
@weichx Thanks for sharing.. will see what can I steal from it :)
@weichx May be you tried it?... I was thinking, what is the fastest route from the byte[]
array to the method.
I am looking at something like https://source.dot.net/#System.Private.CoreLib/src/System/Reflection/Emit/DynamicILGenerator.cs,901
probably just getting the m_Code
field yourself and not using the methods. Also be aware that the Mono implementation looks different, there is a very good chance you'll need two different implementations of this or one of the platforms won't work.
If you'd like to use private/internal methods without reflection, this maybe usefull: https://github.com/aelij/IgnoresAccessChecksToGenerator
Use #375 in .NET 8
Nope for the #375 until UnsafeAccessorTypeAttribute is supported (likely in .NET 9)