Search Unity

C# Performance Tips

Discussion in 'Scripting' started by Rukas90, May 30, 2018.

  1. Rukas90

    Rukas90

    Joined:
    Sep 20, 2015
    Posts:
    169
    Hello everyone,

    Decided to start this thread to write some scripting optimization tips. Hopefully someone will find this useful.

    Results:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public static class ExtensionsManager {
    6.  
    7.     public static float Distance (Vector3 a, Vector3 b)
    8.     {
    9.         Vector3 vector;
    10.         float distanceSquared;
    11.  
    12.         vector.x = a.x - b.x;
    13.         vector.y = a.y - b.y;
    14.         vector.z = a.z - b.z;
    15.  
    16.         distanceSquared = vector.x * vector.x + vector.y * vector.y + vector.z * vector.z;
    17.  
    18.         return (float)System.Math.Sqrt(distanceSquared);
    19.     }
    20. }
    21.  
    22. public class DistancePerformanceTest : MonoBehaviour {
    23.  
    24.     Vector3 v1 = new Vector3 ( 15, 20, 52 );
    25.     Vector3 v2 = new Vector3 (-100, 10, 80);
    26.  
    27.     // Update is called once per frame
    28.     void Update () {
    29.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    30.         Test1();
    31.         UnityEngine.Profiling.Profiler.EndSample();
    32.  
    33.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    34.         Test2();
    35.         UnityEngine.Profiling.Profiler.EndSample();
    36.  
    37.         UnityEngine.Profiling.Profiler.BeginSample("Test 3");
    38.         Test3();
    39.         UnityEngine.Profiling.Profiler.EndSample();
    40.     }
    41.  
    42.     void Test1 ()
    43.     {
    44.         for (int i = 0; i < 100000; i++)
    45.         {
    46.             float distance = Vector3.Distance(v1, v2);
    47.         }
    48.     }
    49.  
    50.     void Test2 ()
    51.     {
    52.         for (int i = 0; i < 100000; i++)
    53.         {
    54.             float distance = ExtensionsManager.Distance(v1, v2);
    55.         }
    56.     }
    57.  
    58.     void Test3 ()
    59.     {
    60.         for (int i = 0; i < 100000; i++)
    61.         {
    62.             float distance = Vector3.SqrMagnitude(v2 - v1);
    63.         }
    64.     }
    65. }
    66.  
    (Times ms)
    Test1: 10.75
    Test2: 5.51
    Test3: 8.20

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class MathfPerformance : MonoBehaviour {
    6.  
    7.     // Update is called once per frame
    8.     void Update()
    9.     {
    10.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    11.         Test1();
    12.         UnityEngine.Profiling.Profiler.EndSample();
    13.  
    14.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    15.         Test2();
    16.         UnityEngine.Profiling.Profiler.EndSample();
    17.     }
    18.  
    19.     void Test1 ()
    20.     {
    21.         for (int i = 0; i < 10000; i++)
    22.         {
    23.             float value = Mathf.Abs(-10.5f);
    24.         }
    25.     }
    26.  
    27.     void Test2 ()
    28.     {
    29.         for (int i = 0; i < 10000; i++)
    30.         {
    31.             float value = System.Math.Abs(-10.5f);
    32.         }
    33.     }
    34. }
    35.  
    (Times ms)
    Test1: 0.76
    Test2: 0.40

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class QuaternionPerformance : MonoBehaviour {
    6.  
    7.     // Update is called once per frame
    8.     void Update()
    9.     {
    10.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    11.         Test1();
    12.         UnityEngine.Profiling.Profiler.EndSample();
    13.  
    14.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    15.         Test2();
    16.         UnityEngine.Profiling.Profiler.EndSample();
    17.     }
    18.  
    19.     void Test1 ()
    20.     {
    21.         for (int i = 0; i < 10000; i++)
    22.         {
    23.             Quaternion rot = Quaternion.Euler(new Vector3(1, 6, 3));
    24.         }
    25.     }
    26.  
    27.     void Test2 ()
    28.     {
    29.         for (int i = 0; i < 10000; i++)
    30.         {
    31.             Vector3 v;
    32.             v.x = 1;
    33.             v.y = 6;
    34.             v.z = 3;
    35.             Quaternion rot = Quaternion.Euler(v);
    36.         }
    37.     }
    38. }
    39.  
    (Times ms)
    Test1: 2.93
    Test2: 1.89

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TagsPerformance : MonoBehaviour {
    6.  
    7.     bool yes = false;
    8.  
    9.     // Update is called once per frame
    10.     void Update()
    11.     {
    12.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    13.         Test1();
    14.         UnityEngine.Profiling.Profiler.EndSample();
    15.  
    16.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    17.         Test2();
    18.         UnityEngine.Profiling.Profiler.EndSample();
    19.     }
    20.  
    21.     void Test1 ()
    22.     {
    23.         for (int i = 0; i < 10000; i++)
    24.         {
    25.             if (gameObject.CompareTag("Player"))
    26.             {
    27.                 yes = true;
    28.             }
    29.         }
    30.     }
    31.  
    32.     void Test2 ()
    33.     {
    34.         for (int i = 0; i < 10000; i++)
    35.         {
    36.             if (gameObject.tag == "Player")
    37.             {
    38.                 yes = true;
    39.             }
    40.         }
    41.     }
    42. }
    43.  
    (Times ms)
    Test1: 2.44
    Test2: 3.54

    (GC Alloc)
    Test1: 0
    Test2: 410.2 KB


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class ChangePosition : MonoBehaviour {
    6.  
    7.     // Update is called once per frame
    8.     void Update()
    9.     {
    10.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    11.         Test1();
    12.         UnityEngine.Profiling.Profiler.EndSample();
    13.  
    14.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    15.         Test2();
    16.         UnityEngine.Profiling.Profiler.EndSample();
    17.     }
    18.  
    19.     void Test1 ()
    20.     {
    21.         for (int i = 0; i < 10000; i++)
    22.         {
    23.             Vector3 b = new Vector3(1, 2, 3);
    24.         }
    25.     }
    26.  
    27.     void Test2 ()
    28.     {
    29.         for (int i = 0; i < 10000; i++)
    30.         {
    31.             Vector3 a;
    32.             a.x = 1;
    33.             a.y = 2;
    34.             a.z = 3;
    35.         }
    36.     }
    37. }
    38.  
    (Times ms)
    Test1: 0.24
    Test2: 0.13
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Of these, the only one I found useful/interesting was the first one - I'd be interested to see what the source code of Vector3.sqrMagnitude looks like that it could actually be slower than an operation that includes a square root. Or, I suspect, maybe that additional time has to do with the fact that it's bridging into native code? In either case, it will make me think twice about using the sqrMagnitude optimization that is taken as common wisdom.

    I guess the gcalloc of the .tag comparison is good to know, for those not aware of it.

    As for the other examples, though, were I to review this code in a project I was working on, I would reject the code outright.

    1) When you're dealing with amounts of time this tiny, the profiler is not a particularly reliable measure of performance. CPU's and compilers do so many optimizations to code under the hood that any minuscule change to the way a method is called may hugely affect the results, which means these results cannot be relied on in other situations.

    2) The amount of time you're saving on these is inconsequential in the context of C# code. If you're doing enough of these operations to matter (millions of times per frame), you need to rethink your entire approach to whatever you're working on, not optimize down a picosecond on each operation. Either use a different technique entirely, or offload processing to the video card or something.

    3) Making your code less readable and maintainable to save 0.000000104 seconds on a Euler operation (yes, that's how it actually maths out on that one) is not good coding practice. Code maintainability is almost always preferable to micro-optimizations.
     
    lordofduct likes this.
  3. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Most of it basically boils down to something that has been reported multiple times already. Using the overloaded constructors for the struct classes appear to have a higher performance impact than having a default vector + settings its values one by one.

    Many of the built-in operations for vectors use the overloaded constructors and therefore inherently suffer from this impact.

    This appears to be the reason why sqrMagnitude does not make the race here. Internally, it's simple math, but the argument you provide (v1-v2) calls the -operator of Vector3, which again calls an overloaded constructor...
     
    lordofduct likes this.
  4. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    That is an odd performance hit for a 3D real time engine to have. o_O

    Seems like you can vote to get this looked at, but it currently only has 152 votes.

    [Edit: 153 now I've just added mine :)]
     
    Aka_ToolBuddy likes this.
  5. mgear

    mgear

    Joined:
    Aug 3, 2010
    Posts:
    9,408
    for Math.abs, it could be even faster doing it manually (at least with Mathf.Min/Max it was)
    Code (CSharp):
    1. float a = -10.5f;
    2. float b = 5f;
    3. float r = a<b?a:b; // Mathf.Min(a,b)

    For replacing bounds.IntersectRay(), i think this was faster with lots of bounds to check (dont have the results here now..)
    https://gist.github.com/unitycoder/8d1c2905f2e9be693c78db7d9d03a102
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I'm with StarManta and Suddoha on this one.

    CompareTag is something newbs should definitely be introduced to!

    And the 'distance' one is a bit unintuitive, but if you think about it (like suddoha points out), it's realted the the fact that you used the "-" operator which uses a struct constructor overload, which can be sort of slow.

    note, the unity Vector3 does not call internally and just does the calculation in C#. It's the struct constructor definitely slowing it down:
    Code (csharp):
    1.  
    2.     public static float Distance(Vector3 a, Vector3 b)
    3.     {
    4.       Vector3 vector3 = new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
    5.       return Mathf.Sqrt((float) ((double) vector3.x * (double) vector3.x + (double) vector3.y * (double) vector3.y + (double) vector3.z * (double) vector3.z));
    6.     }
    7.  
    8.     public static float SqrMagnitude(Vector3 vector)
    9.     {
    10.       return (float) ((double) vector.x * (double) vector.x + (double) vector.y * (double) vector.y + (double) vector.z * (double) vector.z);
    11.     }
    12.  
    But by demonstrating that your Distance implementation is technically faster, it's not really pointing out why. And a custom implementation of SqrMagnitude can really show why:
    Code (csharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public static class ExtensionsManager
    7. {
    8.  
    9.     public static float Distance(Vector3 a, Vector3 b)
    10.     {
    11.         Vector3 vector;
    12.         float distanceSquared;
    13.  
    14.         vector.x = a.x - b.x;
    15.         vector.y = a.y - b.y;
    16.         vector.z = a.z - b.z;
    17.  
    18.         distanceSquared = vector.x * vector.x + vector.y * vector.y + vector.z * vector.z;
    19.  
    20.         return (float)System.Math.Sqrt(distanceSquared);
    21.     }
    22.  
    23.     public static float SqrDistance(Vector3 a, Vector3 b)
    24.     {
    25.         float x = a.x - b.x;
    26.         float y = a.y - b.y;
    27.         float z = a.z - b.z;
    28.         return x * x + y * y + z * z;
    29.     }
    30. }
    31.  
    32. public class DistancePerformanceTest : MonoBehaviour
    33. {
    34.  
    35.     Vector3 v1 = new Vector3(15, 20, 52);
    36.     Vector3 v2 = new Vector3(-100, 10, 80);
    37.  
    38.     // Update is called once per frame
    39.     void Update()
    40.     {
    41.         UnityEngine.Profiling.Profiler.BeginSample("Test 1");
    42.         Test1();
    43.         UnityEngine.Profiling.Profiler.EndSample();
    44.  
    45.         UnityEngine.Profiling.Profiler.BeginSample("Test 2");
    46.         Test2();
    47.         UnityEngine.Profiling.Profiler.EndSample();
    48.  
    49.         UnityEngine.Profiling.Profiler.BeginSample("Test 3");
    50.         Test3();
    51.         UnityEngine.Profiling.Profiler.EndSample();
    52.  
    53.         UnityEngine.Profiling.Profiler.BeginSample("Test 4");
    54.         Test4();
    55.         UnityEngine.Profiling.Profiler.EndSample();
    56.     }
    57.  
    58.     void Test1()
    59.     {
    60.         for (int i = 0; i < 100000; i++)
    61.         {
    62.             float distance = Vector3.Distance(v1, v2);
    63.         }
    64.     }
    65.  
    66.     void Test2()
    67.     {
    68.         for (int i = 0; i < 100000; i++)
    69.         {
    70.             float distance = ExtensionsManager.Distance(v1, v2);
    71.         }
    72.     }
    73.  
    74.     void Test3()
    75.     {
    76.         for (int i = 0; i < 100000; i++)
    77.         {
    78.             float distance = Vector3.SqrMagnitude(v2 - v1);
    79.         }
    80.     }
    81.  
    82.     void Test4()
    83.     {
    84.         for (int i = 0; i < 100000; i++)
    85.         {
    86.             float distance = ExtensionsManager.SqrDistance(v1, v2);
    87.         }
    88.     }
    89. }
    90.  
    Results:
    Code (csharp):
    1.  
    2. Test1 - 5.68
    3. Test2 - 3.83
    4. Test3 - 4.04
    5. Test4 - 1.95 <-- there's the SqrDistance pulling out ahead
    6.  
    This was on my machine, a Core i7 2700K (a bit older... but mid to higher end for when it came out).

    You might notice though that the speeds were actually comparable for the Distance vs SqrMagnitude. And that the custom SqrDistance really pulled ahead here.

    Thing is, the reason why on my machine SqrDistance and the custom Distance were closer matched is because I ran it in the newer .Net 4.6 compatability. When I bring it back to .Net 2.0 it starts to bounce around a lot getting pretty extreme at times, but averaging about:

    Code (csharp):
    1.  
    2. Test1 - 6.25
    3. Test2 - 4.20
    4. Test3 - 5.35
    5. Test4 - 3.00
    6.  
    This is probably mostly because the newer .Net 4.6 is sort of faster.

    It's also telling though that my platform still does pretty well compared to OP's machine. The speed differences aren't as pronounced... and this is on a 7 year old computer that I currently have like 4 virtual machines humming away on. It goes to show different rigs don't scale linearly in complexity.

    ...

    And lets not forget this also means that other platforms, especially platforms that use il2cpp, a lot of these mico-optimizations sort of just go away.

    Which is again why I point out the 'SqrDistance' thing. In the end, avoiding a square root that you don't need is always very adventageous, no matter the platform you're on.
     
    Last edited: Jun 5, 2018
    StarManta likes this.
  7. Aka_ToolBuddy

    Aka_ToolBuddy

    Joined:
    Feb 25, 2014
    Posts:
    543
    I implemented those optimizations and others in this free asset https://www.assetstore.unity3d.com/#!/content/120660?aid=1101l3N9P
    It is easy to use, just import it and rebuild your game.
    To be completely aware of its limitations, please read the asset description.
     
    Doug_B likes this.
  8. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,882
    Slightly off-topic, am I the only one put off by extension methods being put in a class named "ExtensionsManager"?

    This class does not "manage" extensions, it contains them. I'm particularly sensitive when any class gets named "Manager" because it doesn't add anything to the name. Could as well be "ExtensionsSomething". Plainly naming the class "Extensions" would be a much better fit and isn't misleading.

    Btw, my rule for extension classes is: <ClassNameExtensionIsFor>Extensions in the same namespace.

    For example:
    namespace UnityEngine { static class TransformExtensions{..} }

    Extensions could be shortened to Ext if there's verbosity concern.
     
    Doug_B likes this.
  9. Doug_B

    Doug_B

    Joined:
    Jun 4, 2017
    Posts:
    1,596
    Seems reasonable enough. It looks like others on the Microsoft forums are of a similar opinion. :)
     
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    I definitely agree with the over use of the 'Manager'. Along with many other coding sludge I've seen over the years I've been doing this.

    Seldom have the energy to chat about it though since it often gets into such a subjective realm.
     
  11. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    I assume these tips were gathered AFTER you've profiled your game to find where the real bottlenecks were.

    Right?

    :D
     
    Ryiah likes this.