Wayback Machinekoobas.hobune.stream
May JUN Jul
Previous capture 12 Next capture
2021 2022 2023
2 captures
12 Jun 22 - 14 Jun 22
sparklines
Close Help
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
1
Question by Vukelic · May 18, 2020 at 02:12 AM · performancereflectionil2cppexpression

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: alt text
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 :(

    Thank you

il2cppperformance.png (51.8 kB)
Comment
Add comment · Show 3
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image nixcry · Nov 08, 2021 at 02:08 AM 0
Share

Exactly the same issue. Being stuck for 2 weeks here... Have you found a solution? Especially option 3 you mentioned: " 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? ". That looks promising! Have you tried it out?

avatar image Vukelic nixcry · Nov 08, 2021 at 08:01 AM 0
Share

Yes, it's working like that now. Code gets generated whenever you save any code (and Unity reimports it). It's a bit clunky in dev time but it works great in runtime.

avatar image nixcry Vukelic · Nov 09, 2021 at 01:00 AM 0
Share

Congrats!..Still confused about how did you manage it. Could you share your code here?

2 Replies

· Add your reply
  • Sort: 
avatar image
1
Best Answer

Answer by Bunny83 · May 18, 2020 at 03:03 AM

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.

Comment
Add comment · Show 3 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image Vukelic · May 20, 2020 at 09:47 AM 0
Share

Thank you very much, this was probably the way to do it from the start.

avatar image Nuvix · Sep 06, 2021 at 02:57 PM 0
Share

I just wanted to add that boxing should be avoidable here. Instead of using Func<object> or similar in your dictionaries, just use Delegate. Then, instead of casting your delegate's arguments and return values, cast the delegate itself to the proper delegate type.

avatar image Bunny83 Nuvix · Sep 06, 2021 at 04:35 PM 0
Share

Sorry but I don't really understand what you wanted to say here ^^. Func<object> is a predefined generic delegate type. If a delegate types has a return type, you have to specify which one. Func<object> just defines a delegate type that looks like this

 public delegate object YourDelegateTypeName();

You can not really cast a delegate type into another one, unless covariance or contravariance allows it.


Also keep in $$anonymous$$d that the actual type is not known at compile time, which is the primary problem here. If you know the type, there's no need to do anything like this.

avatar image
0

Answer by Vukelic · Nov 09, 2021 at 01:12 PM

@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[i].type == CompilerMessageType.Error && messages[i].file.StartsWith(outputFolder) && messages[i].message.EndsWith("is inaccessible due to its protection level"))
                 {
                     anythingDeleted = true;
                     DeleteErrorFile(messages[i].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[i].IsClass || allAssemblyTypes[i].IsAbstract || !allAssemblyTypes[i].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 });
         }
     }
Comment
Add comment · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Follow this Question

Answers Answers and Comments

148 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

How to make a system that can be both manually controlled and automatized through a sorto of a simplified coding system? 0 Answers

IL2CPP: PROPERTYINFO.SETVALUE SETS A RANDOM NUMBER TO A NULLABLE VALUE TYPE WHEN NULL IS EXPECTED. 0 Answers

Performance impact of blend shapes 0 Answers

Regarding Update() Performance 0 Answers

C# Invoke vs. Unity SendMessage 2 Answers


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges