IL2CPP compiled expressions performance

Hello,

We are writing a library for Unity which uses reflection to set and get field values on objects. Since it could be called on every game loop it must be very fast.
We were able to find great performing solution, using Expression compiling:

public static Action<object, object> CompileFieldSetter(Type type, FieldInfo fieldInfo)
{
    var ownerParameter = Expression.Parameter(typeof(object));
    var fieldParameter = Expression.Parameter(typeof(object));

    var fieldExpression = Expression.Field(Expression.Convert(ownerParameter, type), fieldInfo);

    return Expression.Lambda<Action<object, object>>(
        Expression.Assign(
            fieldExpression, 
            Expression.Convert(fieldParameter, fieldInfo.FieldType)), 
        ownerParameter, fieldParameter).Compile();
}

public static Func<object, object> CompileFieldGetter(Type type, FieldInfo fieldInfo)
{
    var ownerParameter = Expression.Parameter(typeof(object));

    var fieldExpression = Expression.Field(Expression.Convert(ownerParameter, type), fieldInfo);

    return Expression.Lambda<Func<object, object>>(
        Expression.Convert(fieldExpression, typeof(object)),
        ownerParameter).Compile();
}

This code generates functions for fast setting and getting fields for any object of the given class. We would then cache the functions and call them whenever was needed. It was only 2 times slower than accessing the field directly (which we could not use, cos we needed reflection):

    obj.field = "TEST";
    var value = obj.field;

While reflection (fieldInfo.SetValue() and fieldInfo.GetValue()) was more than 200 times slower than accessing the field directly.

Everything was super duper great, until we found out Google Play requires apps to be built for ARM64, and that is only possible using IL2CPP instead of Mono.
At first, exception was being thrown when compiling the expressions, but we managed to fix it using link.xml file with System.Linq.Expressions.Interpreter.LightLambda preserve set to all.
But then we noticed performance issues. After some profiling this is what we got:

My questions are:

  1. How is it even possible for compiled expression to be slower than reflection (only true on IL2CPP)?
  2. Is there a better alternative for reflection with great performance on IL2CPP?
  3. Since we know all required fields on compile time, is there a way to generate (compile) code for setting and getting the fields on compile time to gain performance?
  4. Pls help :frowning:

Thank you

Well IL2CPP is an AOT compiled platform (Ahead Of Time). So there’s no dynamic code generation possible. I never used the Linq expression classes. However it looks like they probably fallback to an interpreted representation of your expression.instead of a compiled one. Keep in mind that IL2CPP actually cross compiles all your code to C++ and your final code is actual compiled C++ native code.

You probably should be able to write some simply code generator that you can run in the editor that will generate your getter and setter methods for each field explicitly based on reflection in the editor. If you also want to be able to access private fields the best approach is usually to use a partial class. That way your code generator can automatically create / update a seperate partial class file with all those getter and setter methods. Since they are technically part of the same class they have direct access to all fields.

Just for example imagine a class like this:

public partial class MyClass
{
    private int myIntField;
    public string myStringField;
}

Your code generator could use reflection inside the editor and generate a seperate file that would look something like that:

public partial class MyClass
{
    public static Dictionary<string, Func<object, object>> s_Getters;
    public static Dictionary<string, Action<object, object>> s_Setters;
    static MyClass()
    {
        s_Getters = new Dictionary<string, Func<object, object>>();
        s_Getters.Add("myIntField", myIntField_Get);
        s_Getters.Add("myStringField", myStringField_Get);
        s_Setters= new Dictionary<string, Action<object, object>>();
        s_Setters.Add("myIntField", myIntField_Set);
        s_Setters.Add("myStringField", myStringField_Set);
    }
    private static object myIntField_Get(object aObj)
    {
        return ((MyClass)aObj).myIntField;
    }
    private void myIntField_Set(object aObj, object aValue)
    {
        ((MyClass)aObj).myIntField = (int)aValue;
    }
    private object myStringField_Get(object aObj)
    {
        return ((MyClass)aObj).myStringField;
    }
    private void myStringField_Set(object aObj, object aValue)
    {
        ((MyClass)aObj).myStringField = (string)aValue;
    }
}

So you just need some code that generates this code based on the reflection / field info of your class. So you provide your own “reflection” dictionary for getter and setter methods. However keep in mind that since you use object for ever field type, you get boxing / unboxing for primitive types. Also it should be clear what when using one of those methods on the wrong object you get an invalid cast exception. However using your linq expressions you have the exact same issues.

Note that in my case the generated part of the class implements the static constructor for that class. So this does only work as long as your actual class doesn’t require the static constructor for other things. It might be better to just implement an “ordinary” static method to initialize the dictionaries which you call somewhere manually. This calling could be done through reflection at the start.

All in all I’m not sure if that detour is worth all the work. Are you sure you really need such a feature? Keep in mind that this solution only works for your own classes since you have to declare them as partial. In case you want to support all kind of types this of course wouldn’t work. However a similar approach can be used just without the whole partial thing. That just means you would create a completely seperate class to implement those getter and setter methods. Though keep in mind that you can only access public members from outside the class.

Another common way is to use System.Delegate.CreateDelegate to create a delegate for the getter and setter methods of a property. However those are quite strict when it comes to types. So you can not turn an “int” property setter into a delegate that takes an object as parameter.

@nixcry here is the main part for recognizing when to generate the code. I don’t really remember which part is for what sorry, I wrote this a long time ago. I hope it helps.

internal class CodeGeneratorManager
    {
        private static string outputFolder = "Assets\\Haste-AutoGenerated\\";

        private static List<string> ignoreAssemblies = new List<string>() { "UnityEngine.TestRunner", "UnityEngine.UI", "Unity.TextMeshPro", "Unity.Timeline" };

        private static bool reloadAssetsInNextFrame = false;

        private static MethodInfo unityLogSticky;
        private static MethodInfo unityRemoveLogEntriesByIdentifier;
        private static int logIdentifier = 3759473;

        [InitializeOnLoadMethod]
        static void Init()
        {
            AssemblyReloadEvents.afterAssemblyReload += OnScriptsReloaded;
            CompilationPipeline.assemblyCompilationFinished += CompilationPipeline_assemblyCompilationFinished;
            EditorApplication.update += Update;

            unityLogSticky = typeof(UnityEngine.Debug).GetMethod("LogSticky", BindingFlags.NonPublic | BindingFlags.Static);
            unityRemoveLogEntriesByIdentifier = typeof(UnityEngine.Debug).GetMethod("RemoveLogEntriesByIdentifier", BindingFlags.NonPublic | BindingFlags.Static);
        }

        private static void CompilationPipeline_assemblyCompilationFinished(string assemblyPath, CompilerMessage[] messages)
        {
            var anythingDeleted = false;
            for (int i = 0; i < messages.Length; i++)
            {
                if (messages<em>.type == CompilerMessageType.Error && messages_.file.StartsWith(outputFolder) && messages*.message.EndsWith("is inaccessible due to its protection level"))*_</em>

{
anythingDeleted = true;
DeleteErrorFile(messages*.file);*
}
}

if (anythingDeleted)
{
reloadAssetsInNextFrame = true;
}
}

private static void Update()
{
if (reloadAssetsInNextFrame)
{
AssetDatabase.Refresh();
reloadAssetsInNextFrame = false;
}
}

private static void DeleteErrorFile(string filePath)
{
if (filePath != null)
AssetDatabase.DeleteAsset(filePath);
}

public static void OnScriptsReloaded()
{
ClearLogErrors();

var allAssemblyTypes = CompilationPipeline.GetAssemblies()
.Where(a => !(a.name.Contains(“Editor”) || a.name.Contains(“editor”)))
.Where(a => !ignoreAssemblies.Contains(a.name))
.Select(a => System.Reflection.Assembly.LoadFrom(a.outputPath))
.SelectMany(o => o.GetTypes()).ToArray();

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();

for (int i = 0; i < allAssemblyTypes.Length; i++)
{
if (!allAssemblyTypes.IsClass || allAssemblyTypes_.IsAbstract || !allAssemblyTypes*.IsSubclassOf(typeof(NetworkComponent)))*
continue;_

* // give assembly to generators to find getters and setters and generate the code*
}

DeleteAllNonExistingFiles();
GenerateAllFiles();

stopwatch.Stop();
UnityEngine.Debug.Log($“Finished generating code. Generation took {stopwatch.ElapsedMilliseconds} ms.”);
}

private static void DeleteAllNonExistingFiles()
{
if (!Directory.Exists(outputFolder))
return;

foreach (var path in Directory.GetFiles(outputFolder))
{
if (path.EndsWith(“.meta”))
continue;

// continue if file will be generated, else delete it

AssetDatabase.DeleteAsset(path);
}
}

private static void GenerateAllFiles()
{
foreach(var generator in generators)
generator.Value.ToFile(outputFolder);
}

private static void LogError(string message)
{
if (unityLogSticky != null)
unityLogSticky.Invoke(null, new object[] { logIdentifier, LogType.Error, LogOption.NoStacktrace, message, null });
else
UnityEngine.Debug.LogFormat(LogType.Error, LogOption.NoStacktrace, null, message);
}

private static void ClearLogErrors()
{
if (unityRemoveLogEntriesByIdentifier != null)
unityRemoveLogEntriesByIdentifier.Invoke(null, new object[] { logIdentifier });
}
}