Search Unity

Squeezing out Performance: Comparing objects

Discussion in 'Editor & General Support' started by par-002, Apr 16, 2013.

  1. par-002

    par-002

    Joined:
    Jul 4, 2012
    Posts:
    52
    Please understand I come from a background where I had to squeeze as much performance out of a mobile device as possible in order to do the things that were required. So what I'm about to ask help with is legitimate and hopefully I'm not the only one who has run into this.

    Comparisons in high performance computing should be done w/ primitives. The overhead of comparing a string with a string versus comparing a number with a number (short, ushort, int, float, etc) is HUGE. Especially when you could be doing it not just every frame but multiple times per frame.

    Unity's tag is a good idea but its a string.

    What I want to do is compare a Collider's tag to a set of known tags to see what I should do about the collision. I want to use an enum... but from what I've read even enums in C# can have some overhead. Ultimately I'd just use a ushort like I did in ObjC but I do not know enough about C# on how to embed something like that in a Monobehavior (or in the Collider class). I've tried inheriting from the Collider class and passing my own in OnTriggerEnter but I am not having any luck.

    Can anyone give me some examples on how they solved this problem? In a Bejewel type game, comparing strings isnt a big deal... even on a mobile device. But a game that could have a lot of computations per frame will certainly be effected negatively.

    Thanks!

    PAR
     
  2. RonHiler

    RonHiler

    Joined:
    Nov 3, 2011
    Posts:
    207
    Could you apply a hash to the tag string at initialization (caching those values) and use the hashes for comparisons?
     
  3. par-002

    par-002

    Joined:
    Jul 4, 2012
    Posts:
    52
    Thanks for the reply RonHiler but that solution really doesn't help since I still need to hash the tag at every comparison before I compared the hashes themselves.

    So I figured this out and did some profiling with the C# StopWatch class. And the results are exactly as I assumed they would be.

    To start, I created an enum with all of the different types of collisions I could have:

    Code (csharp):
    1.     public enum CollidableType {
    2.         PlayerEntity,
    3.         StaticNeutral_1,
    4.         StaticNeutral_2,
    5.         StaticNeutral_3,
    6.         StaticNeutral_4,
    7.         StaticNeutral_5,
    8.         StaticNegative_1,
    9.         StaticNegative_2,
    10.         StaticNegative_3,
    11.         StaticNegative_4,
    12.         StaticNegative_5,
    13.         StaticPositive_1,
    14.         StaticPositive_2,
    15.         StaticPositive_3,
    16.         StaticPositive_4,
    17.         StaticPositive_5
    18.     }
    Then I created a class that extended Unity's MonoBehavior class for each of the possible collision types and initialized them with their respective enum value. I then attached the "StaticNegative_1" script (class) as a component to the GameObject's that would be colliding.

    Inside my collision OnTriggerStay() method I performed the BEST case scenario for comparing the string "tag" of an object (meaning it would always be the first one I compared) versus the WORST case scenario of snagging my component off of the Collider.GameObject and then using a switch to determine which one this specific Collidable was (meaning I made the "known" StaticNegative_1 enum be the last switch choice). I started up the game and sat my Player object in the middle of one of the collidables. Here are the results:

    As you can see for 2000 OnTriggerStay() calls, the average time in nanoseconds (or close enough) for comparing the string tag w/ the known type was 1,084,580ns (or 1.08458ms). The time for the switch call (including the retrieval of the component) took 2802.85ns (or 0.00280285ms).

    Thats 386 times FASTER! And it only gets worse when you compare multiple "tags" to get what you want.

    Thats huge and can be a very good look into why you should always try and compare primitives instead of strings... especially in a game ;)

    Here is the code I used for the timings in case anyone wants to see it:

    Code (csharp):
    1.     void OnTriggerStay(Collider collidedWith) {
    2.         // PROFILER
    3.         // *****************************
    4.         // *****************************
    5.         // *****************************
    6.         runs++;
    7.        
    8.         // #########################################
    9.         // STRING START
    10.         Stopwatch sw = new Stopwatch();
    11.         sw.Start();
    12.  
    13.         if(collidedWith.CompareTag("StaticPositiveCUBE")) {
    14.             UnityEngine.Debug.Log("PLAYER TRIGGER ENTER and hit StaticPositiveCUBE");
    15.         }
    16.         sw.Stop ();
    17.  
    18.         float totalTicksString = sw.ElapsedTicks * (1000L*1000L*1000L) / Stopwatch.Frequency;
    19.         // #########################################
    20.         // STRING END
    21.        
    22.         sw.Reset();
    23.        
    24.         // ENUM START
    25.         // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    26.         sw.Start ();
    27.         Collidable collidableTestType = collidedWith.gameObject.GetComponent<Collidable>();
    28.         PEngineGlobals.CollidableType testType = collidableTestType.ColliderType();
    29.         switch(testType) {
    30.             case CollidableType.StaticNegative_2: {
    31.                 break;
    32.             }
    33.             case CollidableType.StaticNegative_3: {
    34.                 break;
    35.             }
    36.             case CollidableType.StaticNegative_4: {
    37.                 break;
    38.             }
    39.             case CollidableType.StaticNegative_5: {
    40.                 break;
    41.             }
    42.             case CollidableType.StaticNeutral_1: {
    43.                 break;
    44.             }
    45.             case CollidableType.StaticNeutral_2: {
    46.                 break;
    47.             }
    48.             case CollidableType.StaticNeutral_3: {
    49.                 break;
    50.             }
    51.             case CollidableType.StaticNeutral_4: {
    52.                 break;
    53.             }
    54.             case CollidableType.StaticNeutral_5: {
    55.                 break;
    56.             }
    57.             case CollidableType.StaticPositive_1: {
    58.                 break;
    59.             }
    60.             case CollidableType.StaticPositive_2: {
    61.                 break;
    62.             }
    63.             case CollidableType.StaticPositive_3: {
    64.                 break;
    65.             }
    66.             case CollidableType.StaticPositive_4: {
    67.                 break;
    68.             }
    69.             case CollidableType.StaticPositive_5: {
    70.                 break;
    71.             }
    72.             case CollidableType.StaticNegative_1: {
    73.                 UnityEngine.Debug.Log("PLAYER TRIGGER ENTER and hit StaticPositiveCUBE");
    74.                 break;
    75.             }
    76.             default: {
    77.                 break;
    78.             }
    79.         }
    80.         sw.Stop ();
    81.         float totalTicksEnum = sw.ElapsedTicks * (1000L*1000L*1000L) / Stopwatch.Frequency;
    82.         // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    83.         // ENUM END
    84.        
    85.         totalTicksStrings += totalTicksString;
    86.         totalTicksEnums += totalTicksEnum;
    87.        
    88.         float avgTicksStrings = totalTicksStrings / runs;
    89.         float avgTicksEnums = totalTicksEnums / runs;
    90.        
    91.         UnityEngine.Debug.Log("RUNS: [" + runs + "]\tSTRING AVG TIME NS: [" + avgTicksStrings + "]\tENUM AVG TIME NS: [" + avgTicksEnums + "]");
    92.        
    93.         // ******************************************************************
    94.         // ******************************************************************
    95.         // ******************************************************************
    96.     }
    PAR
     
    Last edited: Apr 16, 2013
  4. RonHiler

    RonHiler

    Joined:
    Nov 3, 2011
    Posts:
    207
    No, you don't. Why would you recreate the hash for every collision? They won't ever change. Do it once, at initialization. If you are instancing all your objects at load-up (like you should be) and not instancing anything while the game is playing, you can do these calculations while the loading screen is up, and never have to do it during play-time.

    You create the hash at initialization (in the Awake() function) and store it. Do this for all your objects that have a collider. You can then use those stored values for ALL your comparisons for the rest of the level. I think this gives you exactly what you want, an int vs int comparison.

    The only tricky bit is to make sure you use a hash algorithm that won't give you duplicates. I know that there are algorithms that are very very unlikely to produce duplicates (given different inputs), I'm not sure if there are ones that are guaranteed not to do so.

    Unless I'm not understanding what you are saying (possible!), that seems to solve your problem. I've used this same technique myself for exactly the same reason (in C++ code, not in Unity, but the idea is the same).

    I think that would be faster than using Enums (although perhaps not appreciably so, you'd have to test it).
     
  5. par-002

    par-002

    Joined:
    Jul 4, 2012
    Posts:
    52
    Hey Ron, I think we're saying the same thing in a sense :)

    But regardless of the exact implementation, the difference in lookup times is very significant.

    PAR
     
  6. RonHiler

    RonHiler

    Joined:
    Nov 3, 2011
    Posts:
    207
    Yeah, probably. In that you should never ever compare strings, we are in complete agreement :)

    The ENUM thing is probably fine. You are essentially doing what I suggested, except instead of generating a hash, you are assigning a value manually. No real difference. The only question is how much overhead there is in using an ENUM lookup versus using a straight primitive type (and I really have no idea if there is any at all!).

    Just as a quick bit of advice on your code (if you want it). If you are in performance critical loops, you could speed it up, possibly significantly. You are running this line inside your loop:

    Collidable collidableTestType = collidedWith.gameObject.GetComponent<Collidable>();

    Rather than using GetComponent (which performs another search), you could instead cache that value (again, at initialization) in a script attached to the collidee game objects (make it a property with a get() statement). That way you can remove that search from your inner loop. Give it a try and see how much of a performance increase you get.
     
  7. par-002

    par-002

    Joined:
    Jul 4, 2012
    Posts:
    52
    I switched the enum to just plain ushort declarations and the performance wasn't much different. TBH it was easier to use the enum since I could pass it around as a type. But the biggest thing in doing it this way is that I am not tied to the Tag Manager ;) Doing as much as possible in code is a plus in my mind.

    As for the GetComponent, yes you are right. Its definitely another search that I can remove by saving the reference somewhere easily accessible. One thing I am doing as well is pooling GameObjects instead of recreating them so managing where all of the references are can get somewhat daunting if I don't do it right.

    PAR