Reducing Memory Usage in Unity, C# and .NET/Mono

Unity on iOS uses an early version of Mono’s heap manager. This manager doesn’t do packing, so if you fragment your heap, it’ll just grab a new one for you. I am under the impression the Unity boffins are working on a new heap manager to get around this problem, but for now a game with no memory leaks can end up consuming ever-increasing amounts of memory. C# is a fun language that allows you to quickly write powerful code without sacrificing readability. However the downside to this is that writing natural C# code produces a lot of garbage collection. The only way around this is to eliminate or reduce your heap allocations. I’ve compiled a handy list ways to do this without reducing functionality. The end effect is that your C# code looks much more like C++, and so you lose some of that C# power, but such is life. As an added bonus, heap allocs are inherently more CPU-intensive than stack allocs, so you’ll probably save some frame time as well. To target your efforts, the Unity profiler can help you functions that make a lot of allocations. It’s not a lot of info, but it’s there. Open the profiler and run the game,  select the CPU profiler, and tap the GC Alloc column to sort by the worst offenders. Apply these guidelines to those functions first.

  • Avoid using foreach(). It calls GetEnumerator() on your list type, which will allocate an enumerator on the heap just to throw it away. You’ll have to use the more verbose C++-style for(;;) syntax. EDIT: Unless you’re foreach-ing over an array. That causes no allocations. I guess that’s a special case that becomes syntactic sugar for a for(…){}? Thanks to Ian Horswill for pointing that out.
  • Avoid strings. Strings are immutable in .NET and allocated on the heap. You can’t manipulate them in-place like C. For UI, use StringBuilders to build up strings in a memory-efficient manner, delaying the conversion to string until as late as possible. You can use them as keys because literals should point to the same instance in memory, but don’t manipulate them too much.
  • Use structs. Struct types in mono are allocated on the stack, so if you have a utility class that won’t leave scope, make it a struct. Remember structs are passed by value, so you will have to prefix a parameter with ref to avoid any copy costs.
  • Replace scope-bound fixed-size arrays with structs. If you have a fixed-size array that doesn’t leave scope, consider either replacing it with a member array that you can reuse, or create a struct with fields that mirror it. I replaced a Vector3[4] that was allocated every time you called our spline class with a ControlList struct that had four fields. I then added an this[] property for index-based access. This saved a ton of allocations because it was such a high-frequency function.
  • Favour populating lists passed as ref parameters over returning new lists. This sounds like you’re not saving anything – you still need to heap-alloc the list you pass in, right? – but it allows us to, where necessary, make the next particularly ugly optimisation:
  • Consider storing the scope-local storage of a high-frequency function as a member variable. If your function needs a big list every time it’s called, make that list a member variable so the storage persists between frames. Calling .Clear() on a C# list won’t delete the buffer space so you have no/less allocs to do next frame. It’s ugly and makes the code less readable so needs a good comment, but can make a big difference on heavy lifters.
  • Avoid IEnumerable extension methods. It goes without saying that most Linq IEnumerable extension methods, as handy as they are, will create new allocations. However I was surprised that .Any(), called on an IList<>, which I expected to just be a virtual function to Count > 0, triggered an allocation. It’s the same for other funcs that should be trivial on an IList, like First() and Last(). If anyone can illuminate me on why this is I’d appreciate it. Because of this, and the foreach() restriction, I’d go as far as to say avoid the IEnumerable<> abstraction in your interfaces, and use IList<> instead.
  • Minimise use of function pointers. Putting a class method in a delegate or a Func<> causes it to be boxed, which triggers an allocation. I can’t find any way to store a link to a method without boxing. I’ve left most of my function pointers in there because they’re a massive boon to decoupling, and I’ll just have to live with the allocs, but I’ve removed some.
  • Beware cloned materials. If you get the material property of any renderer, the material will be cloned even if you don’t set anything on it. This material isn’t GC’d, and is only cleared up when you either change levels or call Resources.UnloadUnusedAssets(). Use myRenderer.sharedMaterial if you know you don’t want to adjust the material.
  • Don’t use raw structs as keys in dictionaries unless... If you use a Dictionary<K,V> type, and your key value is a struct, fetching a value from the dictionary using TryGetValue() or the index accessor can, somehow, cause an allocation. To avoid this, implement IEquatable<K> for your struct. I guess the dictionary is creating some default equality comparer every time. Thanks to Ryan Williams for finding this one.
  • 2014/5/8 Don’t overuse Enum.GetValues() or myEnumValue.ToString()Enum.GetValues(typeof(MyEnum)) allocates an array with every call. Enum.GetNames() does something similar. If you’re like me, you’re probably using these heavily, along with .ToString() on enum variables, to do UI, as well as is other places. You can cache both these arrays easily, allowing you to do a for loop over enum values as well as cachedEnumNames[(int)myEnumValue]. This doesn’t work if your enum values are manually set, eg flags.

Feel free to add more in the comments.

Shamless plug: Easily add human-like and emergent decision making to your Unity3d game with DecisionFlex

Advertisements

33 thoughts on “Reducing Memory Usage in Unity, C# and .NET/Mono

    • Was about to talk about this. +1 for Mark’s reply. It’s also a prerequisite for Instancing for Unity 5.4+.
      Setting those MPBs leaves the original material the same and makes it possible to batch the SetCalls.

  1. Mark do you have a good example of SetPropertyBlock in use? I had researched it before and found that the input parameter type, MaterialPropertyBlock, says that it is used when you’re drawing geometry using Graphics.DrawMesh.

    http://docs.unity3d.com/Documentation/ScriptReference/MaterialPropertyBlock.html

    I’ll mess around with it today and post again what I find. I’ve never found a single example of anyone using this. If you search around for SetPropertyBlock and my name on the Unity community, you’ll find I’ve poked around this for a while. Thanks!

  2. Pingback: Unity的内存优化 | OWNSELF
    • Not entirely sure what you mean by manually. Any throw of an exception will involve allocating the exception and probably some nasty stuff as the stack unwinds. I’m not sure of the effect on mono of exceptions, but coming from C++ by habit I turn them off for release builds.

    • Well, Exceptions should be what they are called: Exceptions.

      If you get multiple exceptions per frame, you are probably doing something wrong. Exceptions will only cause extra allocations and processing power when they are triggered. If the exception is not triggered, there is no performance penalty.

      When you have code, where you expect that the exception is thrown more often than not, you should use “is” keyword instead (usually for casting or null checks) i.e. instead of

      MyClass myClass = (MyClass)baseClass;

      you should use

      if(baseClass is MyClass) {
      MyClass myClass = (MyClass)baseClass);
      myClass.MyClassMethod();
      }

      or use

      MyClass myClass = baseClass as MyClass;
      if(myClass != null) {
      }

      When you expect baseClass to be almost always “MyClass”

      And for objects that can be null always use null checks. Exceptions shouldn’t be triggered in Update/FixedUpdate/LateUpdate methods. You should make sure that everything is correctly initialized in Awake() method (look at it as Constructor replacement in Unity’s component model)

  3. Long story short: Avoid all the stuff that makes C# pleasurable and productive and write hackish, long winded, single-thread reliant, maintenance nightmare code?

    P.S. You forgot object pools… I pool damn near everything that is transient in nature and not a struct.

    • I wouldn’t say “avoid,” just understand the costs. If you’ve got high frequency code, consider these steps.

      And yes it is all the pleasurable stuff. Such is life!

  4. Pingback: Unity的内存优化 | OWNSELF
  5. >>However I was surprised that .Any(), called on an IList, which I expected to just be a virtual function to Count > 0, triggered an allocation.<<

    You're really not working much with C#/.NET, are you? .Any() like every other Extension List/Collection extension methods extends to IEnumerable which OF COURSE forces the implementation of GetEnumerator and IEnumerator and hence an Enumerator class. Enumerable do not have a .Count property, in order to find out the count of an IEnumerator one would have to call IEnumerator.MoveNext() until it returns false, which would be equal to do a foreach loop over the IEnumerator.

    IEnumerable.Any() simply gets the enumerator and calls IEnumerator.MoveNext() next once and returns it’s values, and resets the Enumerator again. An IEnumerator has it’s initial position before the first element. So MoveNext() would move it to the first Element, if one exitsts. If MoveNext() returns false on it’s first call, it means the IEnumerator is empty. If it returns true it means it has at least one element is hence is not empty.

    It’s funny how people who don’t have basic understanding of C#/.NET Framework giving “optimization” hints here

    • Thanks for your comment.

      I understood Any() is an extension method on an IEnumerable. But I had (incorrectly) assumed that it would check to see if the IEnumerable was an IList instance, and defer to the Count property, and only fall back to an enumerator if it had too.

      I profiled to ascertain that Any caused a heap alloc, then avoided using it in high-frequency code. I’m not sure what about this is bad advice.

  6. Just to add an last comment: All of the above isn’t really necessary to do in .NET or the current Mono. At least most of it. Because .NET always had a rather good garbage collector. The problem is, Unity3D is using a stoneage old Mono version (2.6 iirc) but Mono development is much further now.

    We have Mono 2.10.x which has a new SGen garbage collector, but Unity refused to update it for over 3 years now, despite a big chunk of the developers crying for the Mono update.

    • Yup, the Unity GC needs to be GCed, pronto. Every developer who *isn’t* crying about it either has something against efficient use of developer time or doesn’t understand how utterly and ridiculously bad 1990’s style “Stop the world and check everything” GCing is.

  7. As far as .Any() is concerned, you should not be surprised that it’s not doing Count > 0; what Any() really does is loop through the IEnumerable until it finds something that satisfies its given condition (e.g. for an IEnumerable containing [0, 1, 1, 2, 3, 5, 8], and an ie.Any(n => n == 1), the Any function will stop working on element #2 of the given IEnumerable). Thus, I assume that the default condition for Any involves determining if there is at least one element in the IEnumerable, which as you point out, would still cause an allocation.

  8. Coming from an internet start-up background where productivity is king, I grew incredibly weary of purposely avoiding foreach and Linq because of allocations and actually *actively choosing* to be less productive and waste development time on writing and maintaining tons of convoluted loops…

    So I put my game on hold developed Smooth.Slinq, a faster-than-Linq-with-more-features-and-no-per-frame-allocations, Linq / Scala inspired enumeration API for Unity.

    Asset Store Link – https://www.assetstore.unity3d.com/#/content/16249

    Slinq Documentation – https://www.smooth-games.com/docs/smooth-slinq/
    Base Documentation (eg: Option, Tuple) – https://www.smooth-games.com/docs/smooth-base/

    Support Forums – https://www.smooth-games.com/forums/

  9. I’ve decided to release Slinq (and Smooth.Compare, a replacement for the System.Collections.Generic.Foo.Default comparers that helps reduce runtime allocations and eliminate JIT expections) as part of Smooth.Foundations under the MIT License.

    The latest version isn’t up on the Asset Store yet, but direct download links to the code and documentation are available on the Smooth Games forums:

    https://www.smooth-games.com/forums/

    • I’m half sorry that this obviously wasn’t a financial success for you, and half extremely happy that you decided to open-source instead of keeping it. *high five*

  10. Pingback: 如何使用 Unity* 规划优化 | Rukawa
  11. Pingback: Unity Mono Runtime – The Truth about Disposable Value Types | Coding Adventures
  12. Regarding to Func, Action and Predicate are no more than just a syntactic sugar for a generic delegate, so there’s no difference between them. The problem is, when you declare a delegate this will create a ‘invisible’ class that will hold the method’s pointer that you can call, and it also will extend “System.MulticastDelegate” so you can code things like:

    Action myAction = a.Method1;
    myAction += b.Method1;

    So the compiler creates a class wrapper to hold the methods that can be call in a invocation list. Due this behavior it’s impossible to just receive a pointer method and call it without encapsulating. Ignoring the multicast delegate, it will always wrapper the method into a delegate class.

  13. Pingback: REDUCING MEMORY USAGE IN UNITY, C# AND .NET/MONO - Ronnie's Development Story
  14. Pingback: Optimization – Les jeux du grenier
  15. Pingback: Unity3D Best Practices – ArgosVu

Leave a Reply to afray Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s