Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

The struct / static typing performance power-up

Discussion in 'iOS and tvOS' started by n0mad, Nov 22, 2009.

  1. n0mad

    n0mad

    Joined:
    Jan 27, 2009
    Posts:
    3,732
    Hey,

    Just wanted to share my experience in great performance increase :

    I'm actually reworking my code to optimize the hell out of it.

    2 things I'm doing so far :

    • * grouping all my class datas into struct blocks.

    For example, before I got :

    Code (csharp):
    1. public class MyClass : MonoBehaviour {
    2.  
    3.  string _objectName;
    4.  int _objectNumber;
    5.  AnotherClass _objectClass;
    6.  
    7.  
    8. }
    Now I got :
    Code (csharp):
    1.  
    2. public struct _objectStruct {
    3.  
    4.  public string _name;
    5.  public int _number;
    6.  public AnotherClass _class;
    7.  
    8. }
    9.  
    10. public class MyClass : MonoBehaviour {
    11.  
    12.  private _objectStruct _object;
    13.  
    14.  
    15. }
    Doing this for everything that could be grouped into a struct, separating different structs from their job. This helps to see things much clearer.
    And according to MSDN, it is better performance (I'll illustrate this later).

    • * replacing all my Classes instances by a single static Superclass.

    Before I got :

    Code (csharp):
    1. public class PositionManager : MonoBehaviour {
    2.  
    3. //yadda yadda
    4.  
    5. }
    6.  
    7. public class ActionManager : MonoBehaviour {
    8.  
    9. //yadda yadda
    10.  
    11. }
    12.  
    13. public class MainClass : MonoBehaviour {
    14.  
    15.  private ActionManager _actionManager;
    16.  private PositionManager _positionManager;
    17.  
    18.  
    19.  void Awake() {
    20.  
    21.   _actionManager = (ActionManager) gameObject.GetComponent       (typeof(ActionManager));
    22.  
    23.   _positionManager = (positionManager) gameObject.GetComponent (typeof(positionManager));
    24.  
    25.  }
    26.  
    27.  void Main() {
    28.  
    29. // stuff using _actionManager  _positionManager
    30.  
    31.  }
    32.  
    33.  
    34. }
    35.  
    36. public class OtherClass : MonoBehaviour {
    37.  
    38.  private PositionManager _positionManager;
    39.  
    40.  void Awake() {
    41.  
    42.   _positionManager = (positionManager) gameObject.GetComponent (typeof(positionManager));
    43.  
    44.  }
    45.  
    46.  void Main() {
    47.  
    48. // stuff using _positionManager
    49.  
    50.  }
    51.  
    52.  
    53. }
    And this kind of code was repeated as many times as there were characters on screen.

    Now I got :

    Code (csharp):
    1. public class Superclass : MonoBehaviour {
    2.  
    3.  static public PositionManager[] _positionManagers;
    4.  static public ActionManager[] _actionManagers;
    5.  
    6.  static private string[] _charNames;
    7.  
    8.  void Awake () {
    9.  
    10.   int _numberOfCharacters = 2;
    11.   _charNames = new string[]{"Player1", "Player2"};
    12.  
    13.   _positionManagers = new PositionManager[_numberOfCharacters];
    14.   _actionManagers = new ActionManager[_numberOfCharacters];
    15.  
    16.  }
    17.  
    18.  
    19.  static public void Create_ActionManager(int _char) {
    20.  
    21.   _actionManagers[_char] =   GameObject.Find(_charNames[_char]).AddComponent(typeof(ActionManager)) as ActionManager;
    22.  
    23.  }
    24.  
    25.  static public ActionManager Get_ActionManager(int _char) {
    26.   return _actionManagers[_char];
    27.  }
    28.  
    29.  static public void Create_PositionManager(int _char) {
    30.  
    31.   _positionManagers[_char] = GameObject.Find(_charNames[_char]).AddComponent(typeof(PositionManager)) as PositionManager;
    32.  
    33.  }
    34.  
    35.  static public PositionManager Get_PositionManager(int _char) {
    36.   return _positionManagers[_char];
    37.  }
    38.  
    39.  
    40. }
    41.  
    42. // skipping ActionManager  PositionManager, they're the same as in "before" example
    43.  
    44. public class MainClass : MonoBehaviour {
    45.  
    46.  
    47.  void Main() {
    48.  
    49.   int _char = _someCharNumber;
    50.  
    51.   Superclass.Create_ActionManager(_char);
    52.   Superclass.Create_PositionManager(_char);
    53.  
    54.   // stuff using Superclass.Get_ActionManager(_char)    Superclass.Get_PositionManager(_char)
    55.  
    56.  }
    57.  
    58.  
    59. }

    In short, I'm dramatically reducing the amount of instances in memory by setting all the storing and calling in a unique class, accessible from anywhere because it is static (= global).


    I'm not done yet with the conversion of all my code, but all I can say is that before that struct / static conversion thing, I was hitting a frametime of 19ms to 23ms on a certain stage of my game (see attached picture).

    Now, I'm hitting 17.2ms to 22ms.


    (All tests on 3GS)

    20ms is an average of 50 FPS.

    17ms is 58 FPS.

    That makes me very optimistic on final FPS, thanks to this kind of code architecture optimization.
     

    Attached Files:

  2. giyomu

    giyomu

    Joined:
    Oct 6, 2008
    Posts:
    1,094
    look interesting 8)

    need to play with struct too I guess ^^,

    thanks for the tip n0mad ;)
     
  3. Poita_

    Poita_

    Joined:
    Dec 18, 2008
    Posts:
    146
    You know that this:

    Is the time spent in your scripts? i.e. what you are optimizing?

    Most of your time is spent skinning your models and general Unity engine work (sending verts to the graphics chip, scene management). You'll get better improvements by trying to reduce draw calls or vertex count of characters.
     
  4. n0mad

    n0mad

    Joined:
    Jan 27, 2009
    Posts:
    3,732
    Actually this is the profile for the already optimized code.

    Also, this is taken from the fastest profile of all. As there is no "min-max-average" time for time spent in code, you could not know those key values that would tell how fastest is it with such an architecture.

    Optimization with struct is made by better memory access, and with static is made by far less instances (less eaten memory), and far less null testing (every time you access an instance, there is a null testing).

    Finally, with so much graphical features and details, 6 draw calls is the strict minimum I can reach. 10k verts total can't be better too if I don't want my chars to look like Virtua Fighter cubes and my background to be pointless.

    I'm judging the performance gain by watching the frametime, not other details, as frametime is the final ... frame time ;)
    (even if it's more relative to different parameters than code time)

    At last, I can really feel an overall performance boost. Plus an important feature that is direct access to any property of any fighter from anywhere (no more getters/setters/infinite parameter calls between each other).

    Actually this is not the first time someone posts struct/static optimization experience in these forums :)
    I've seen those several times here since I joined (last year).


    edit :

    Here I found an old Profiler from the very same level :

    (from this thread)

    In this old version (was already iUnity 1.5), there was much less code, and much less graphical features/manipulations.


    Before :

    mono-scripts> update: 2.0 fixedUpdate: 0.3 coroutines: 0.2

    Now :

    mono-scripts> update: 0.1 fixedUpdate: 0.0 coroutines: 0.1


    Speaks by itself ;)

    One last point, I'm not putting that much horsepower on code as you can see. Essentially, it is hitTests, movement, combos, HUD display update, and background dynamic animation.

    I guess the upgrade would be much more visible in complicated types of games.
     
  5. rozgo

    rozgo

    Joined:
    Feb 7, 2008
    Posts:
    158
    What you are experiencing here is performance increase due to data locality. By using native arrays and structs you are tightly packing all your data into one big chunk of contiguous memory, increasing your chances of hitting your L1 cache.

    .Net (Mono) is a beast and the chances of anything being in cache is almost a miracle. But the profiler shows that their is indeed performance to gain.
     
  6. n0mad

    n0mad

    Joined:
    Jan 27, 2009
    Posts:
    3,732
    That's very interesting :D

    So as I guess that L1 doesn't have a very big size, this perf gain would only work for small sized code engines ?

    (like platform/fighting games, but not RTS/RPG games for example)
     
  7. rozgo

    rozgo

    Joined:
    Feb 7, 2008
    Posts:
    158
    This, in theory, could work with any game. As long as you can find the best algorithm to access your data (like spatial algorithms).

    For instance, a very simple case, imaging having a huge array. And for every consecutive 20 elements you have an entry in another array that says if this bucket (20 elements) is dirty and needs to compute, else skip (all 20). From a list of a thousand elements you'll be able to skip most of it. You will miss a few caches, but most will just be a prefetch away.

    Core engineers are good for these kind of stuff, if you can ask the right questions, they will have a good data structure and spatial algorithm for you.

    I learned that its very important to understand your data and access behavior, in order to get a good solution from a core engineer (or wikipedia). Those dudes are grumpy most of the time.

    EDIT: Look at google for the best example, that its not the size of your data, but the power of your spatial algorithm. Every time I think the iPhone can't handle something, I just think, if google can search the world in miliseconds, I can squeeze in a bit more data. (Not really a symmetric comparison, but what the heck)
     
  8. n0mad

    n0mad

    Joined:
    Jan 27, 2009
    Posts:
    3,732

    Yeah, that's a good comparison.

    Now you made my SuperGeek side grow even bigger :D
    I just love gamemaking.

    Then I can't imagine how a pain should it be for AAA console / pc games to think the way you described with such millions of lines of code.
     
  9. mudloop

    mudloop

    Joined:
    May 3, 2009
    Posts:
    1,107
    Interesting stuff. I'll look into using structs myself.

    I may be completely missing the point here, but I don't get how calling a function like Superclass.Get_PositionManager(_char) can be any faster than having this stored in class properties? If you would have an Update function in your MainClass, and it would need to access the positionmanager, would it call these functions each time? Or would you have the update method in your superclass?
     
  10. n0mad

    n0mad

    Joined:
    Jan 27, 2009
    Posts:
    3,732
    I practically don't use Update() at all, 99% only coroutines, but anyway :

    Static typing puts the object in memory from the beginning to the end of the process. This object is not instanciable, which means you can only access to it by calling it directly from its class : class.MyObjectFunction();

    When you create an instance, not only does it use more memory (sorry but I can't find a proper illustrative link anymore), but you got to multiply it by the amount of classes using it. So in the end, this can take some memory.
    Plus, every time you are accessing an instance, the compiler does a null test to provide an appropriate NullException if instance has not been created.

    Static objects are always there, so they can never send a NullException, therefore don't need null testing, and don't need multiplicity as they are unique.


    Each time you want to access these static objects from elsewhere, you have to type the whole path to access it (Superclass._method(_param)).

    This could be kind of visually heavy when you got tons of operations to perform. A simple way to avoid that is to create shortcut methods :

    MyClass _myFunction(_params) {
    return Superclass._myClass._myFunction(_params)
    }

    Which makes it as visually clear as an instance, except you just put parenthesis next to it.

    Before :

    myClass._myFunction(_params);

    After :

    _myFunction(_params);