C # Memory management for Unity developers (in the previous article)

Source: Internet
Author: User
Tags net thread

This article translated from: C # Memory Management for Unity Developers (Part 1 of 3)

Many games often crash, and in most cases are caused by memory leaks. This series of articles explains in detail the causes of memory leaks, how to find leaks, and how to circumvent them.

I'm going to confess before I start this post. Although I have been a C + + developer for a long time, I have been a secret fan of Microsoft's C # language and. NET Framework. About three years ago, when I decided to leave the wild, C + +-based graphics library and into the civilized world of modern game engines, Unity stood out with a feature that made me choose it without hesitation. Unity does not require you to ' write scripts ' in one language (such as LUA or unrealscript) but to ' program ' in a different language. Instead, it has deep support for mono, which means that all programming can use any. NET language. Oh, I'm so happy! I finally have a legitimate reason to say goodbye to C + + and all my problems have been resolved through automatic memory management. This feature has been built into the C # language and is an integral part of its philosophy. No more memory leaks, no more considerations for memory management! My life will become much easier.

If you have even the most basic experience of using unity or game programming, you know how wrong I am. I've struggled to understand that in game development, you can't rely on automatic memory management. If your game or middleware is complex enough and resource-efficient, Unity development in C # is a bit like going backwards in C + +. Every new unity developer quickly learns that memory management is cumbersome and cannot be simply delegated to the common language runtime (CLR). The Unity Forum and many of the unity-related blogs include a collection of memory techniques and best practice. Unfortunately, not all of these are based on solid facts, and to the best of my knowledge, none of them is comprehensive. In addition, C # experts on websites like StackOverflow often seem to have little patience with the quirky, nonstandard issues faced by unity developers. For these reasons, in this and the following two posts, I try to give an overview of the memory management issues of unity-specific C #, and I would like to introduce some in-depth knowledge.

The first article discusses the. NET and Mono's garbage collection in the world of memory management fundamentals. I also discussed some common sources of memory leaks.
The second article looks at the tools for discovering memory leaks. Unity's Profiler is a powerful tool, but it is also expensive (it seems not in China). Therefore, I will discuss. NET disassembly and common Intermediate language (CIL) to show how you can only use the free tools to discover memory leaks.
The third article discusses C # object pooling. Again, the focus is only on the specific needs that arise in the development of unity/c#.

Restrictions on garbage collection
Most modern operating systems divide dynamic memory into stacks and heaps (1, 2), and many CPU architectures (including your pc/mac and smartphones/tablets) support this distinction in their instruction set. C # supports it by differentiating value types (simple built-in types and user-defined types that are declared as enumerations or structs) and reference types (classes, interfaces, and delegates). The value type is in the heap, and the reference type is allocated on the stack. The heap has a fixed size that is set at the beginning of a new thread. It is usually very small-for example, the net thread in Windows defaults to a 1MB stack size. This memory is used to load the main function and local variables of the thread, and then load and unload functions called by the main function (with their local variables). Some memory may be mapped to the CPU's cache to speed up. As long as the call depth is not too high or the local variable is large, you do not have to worry about stack overflow. The use of this stack is well suited to the concept of structured programming (structured programming).

If the objects are too large to fit on the stack, or if they live longer than the function that created them, the heap should be out at this time. The heap is "everything else"-a memory that can grow with each OS request, and over which the program rules as it wishes (this sentence won't ...). )。 However, although the stack is almost impossible to manage (using only one pointer to remember where the free section begins), the heap fragments will quickly be disrupted from the order in which they were allocated to the order in which you released them. Think of the heap as Swiss cheese, you must remember all the holes! There is no fun at all. Enter automatic memory management. Automatic assignment of tasks-mainly for you to track all the holes on the cheese-is easy and is supported by almost all modern programming languages. The harder part is automatic release, especially when you decide to release it, so you don't have to worry about it.

The latter task is called Garbage Collection (GC). It's not what you tell you. When the runtime environment can release the memory of an object, it is the runtime that keeps track of all object references so that it can be determined--at certain intervals, an object cannot be referenced by your code. Such an object can be destroyed, and its memory will be freed. The GC is still being actively studied by academics, which explains why the GC architecture has changed so much since the. NET Framework version 1.0. However, unity does not use. NET but its open-source cousin, Mono, which has lagged behind its commercial rivals (. net). In addition, unity does not use the latest version of Mono (2.11/3.0) by default, but instead uses version 2.6 (2.6.5, to be exact, on my Windows4.2.2 installation version (edit: This also applies to Unity4.3]). If you're not sure how to verify this yourself, I'll discuss it in the next post.

Significant changes to the GC were introduced after the Mono2.6 version. The new version uses generational garbage collection (generational GC), while 2.6 still employs a less complex Boehm garbage collector (Boehm garbage collector). Modern generational GC performs very well and can even be used in real-time applications (within a certain limit), such as games. On the other hand, the Bum GC works by doing exhaustive search garbage on the heap. At a relatively "rare" time interval (that is, the usual frequency is significantly lower than once per frame). As a result, it is very likely to cause a decrease in frame rate at a certain time interval, thus interfering with the player. Unity's documentation recommends that you call System.GC.Collect () as long as your game enters a stage where the frame rate is not so important (for example, to load a new scene, or to display a menu). However, there are very few opportunities for many types of games, which means that the GC may break in when you don't want it. If that's the case, your only option is to bite the bullet and manage the memory. And that's what's in the rest of this post, as well as the following two posts!

Do your own memory manager

Let us affirm the unity/. NET in the world of "self-management memory" means what. The power you have to influence how memory is allocated is (fortunately) very limited. You can choose to customize the data structures that are classes (always allocated on the heap) or structs (allocated in the stack, unless they are included in a class), and that's all. If you want more avatar, you must use C # 's unsafe keywords. However, unsafe code is just code that cannot be verified, which means that it does not run in Unity Web player and may include some other platforms. Because of this and other reasons, do not use unsafe keywords. Because the above limitations of the stack are also because C # arrays are just System.Array (this is a class) of syntax sugars, you cannot and should not avoid automatic heap allocations. What you should avoid is unnecessary heap allocations, and we'll talk about this in the next (and final) section of this post.

When it comes to release, your strength is just as limited. In fact, the only process that can release a heap object is a GC, and it works invisible. You can affect the time that the last reference to any object goes out of scope in the heap, because before that, the GC cannot touch them. This limitation is of great practical significance, because periodic garbage collection (which you cannot suppress) is often very fast when nothing is released. This fact provides the basis for the various methods of building the object pool, which I discussed in the third post.

Common causes of unnecessary heap allocations

Should you avoid the Foreach loop?

The common advice I often encounter in the Unity Forum and elsewhere is to avoid the Foreach loop and replace it with a for or while. At first glance, the reason seems to be adequate. foreach is really just a syntax sugar because the compiler will preprocess the code like this:

 foreach  (SomeType s in   Somelist) s.dosomething (), .... To something like the the the the following:  using  (Sometype.enumerator enumerator = this  . Somelist.getenumerator ()) { while   ( Enumerator. MoveNext ()) {SomeType s  = (SomeType) enumerator.       Current;    S.dosomething (); }}

in other words, each time you use foreach, a enumerator object is created in the background-a system.collections.ienumerator An instance of the interface. But is it created on the heap or on the stack? This is a good question, because both of them are possible! Most importantly, almost all collection types in the >, dictionary<k, vt getenumerator ()

Matthew Hanlon points out that Microsoft's current C # compiler and unity are using an unfortunate difference between the old mono/c# compilers that compile your scripts. You may know that you can use Microsoft Visual Studio to develop or even compile Unity/mono-compatible code. You only need to place the corresponding assembly in the ' Assets ' directory. All code is executed in the Unity/mono runtime environment. However, the execution results are not the same as who compiled the code. This is an example of the Foreach loop, which I discovered. Although the two compilers will recognize the GetEnumerator () of a collection to return a struct or class, mono/c# has a bug that struct-enumerator boxing to create a reference type.

So do you think you should avoid using the Foreach loop?

    • Do not use when unity compiles for you
    • It can be used to traverse the standard generic collections (list<t> etc) when using the latest compiler. Visual Studio or the free. NET Framework SDK is available, and I guess the latest version of Mono and MonoDevelop is also available.

What happens when you use a Foreach loop to traverse other types of collections while using an external compiler? Unfortunately, there is no unified answer. Use the techniques mentioned in the second post to find out which collections are safe to use with foreach.

Should you avoid closures and LINQ?

You might know that C # provides anonymous functions and lambda expressions (these two are almost but not the same). You can create them with the delegate keyword and the/ = operator, respectively . They are often useful tools, and you are very difficult to avoid when using specific library functions (for example, list<T; Sort ()) or LINQ.

Can anonymous methods and lambda cause memory leaks? The answer is: look at the situation. The C # compiler actually has two completely different ways to handle them. Take a look at the following small pieces of code to understand their differences:

 1  int  result = 0  ;  2  void   Update () { 3  for  (int  i = 0 ; i < 100 ; I++ int , int  > MyFunc = (p) = = P * P;     5  result += MyFunc (i);  6 }} 

As you can see, this code seems to create a myFunc delegate 100 times per frame, each time using it to perform a calculation. But mono allocates memory only when the update () function is first called (52 bytes on my system), and no heap allocations are made in subsequent frames. What's going on? Using the code reflector (which I'll explain in the next post), I'll find that the C # compiler simply replaces MyFunc with a static field of system.func< int, int > class.

Let's make a little change to the definition of this delegate:

  system.func<intint> MyFunc = (p) = = p * i++;

By replacing ' P ' with ' i++ ', we turn a function that can be called ' locally defined ' into a real closure. Closures are the core of functional programming. They bind functions and data-more precisely, non-local variables that are defined outside the function. In the MyFunc example, ' P ' is a local variable but ' I ' is not, it belongs to the scope of the update () function. The C # compiler now has to convert myfunc to functions that can access or even change non-local variables. It does this by declaring (backstage) a new class to represent the reference environment created by MyFunc. The objects of this class are created every time we go through a for loop, so we suddenly have a huge memory leak (2.6kb per frame on my computer).

Of course, the main reason for introducing closures and some other language features in c#3.0 is LINQ. If a closure causes memory leaks, is it safe to use LINQ in the game? Maybe I'm not fit to ask this question because I always avoid using LINQ like a plague. Part of LINQ obviously does not work on systems that do not support real-time compilation (JIT), such as iOS. But from a memory perspective, LINQ is not a good choice either. An incredibly basic expression like this:

1 int[] Array = {1,2,3,6,7,8 };2 voidUpdate () {3ienumerable<int> elements = fromElementinchArray4  byelement Descending5  whereElement >2                    6 SelectElement ...}

Each frame on my system needs to be allocated 68 bytes (enumerable.orderbydescending () allocation,Enumerable.where ()40)! The culprit here is not even a closure , but a IEnumerable extension method: LINQ must create an intermediate array to get the final result, and there is no appropriate system to recycle. That said, but I'm not an expert on LINQ, and I don't know if some of it can be used in practice.

Co-process

If you start a process with Startcoroutine () , you implicitly create an instance of the Unity Coroutine Class (21 bytes) and a enumerator class (16 bytes). It is important that when the process yields and resume are not allocated memory, you only need to limit the startcoroutine () call when the game is running to avoid memory leaks.

String

An introduction to C # and unity memory issues does not mention that strings are incomplete. From a memory perspective, strings are strange because they are both heap-allocated and immutable. When you connect two strings in this way:

 1  void    Update () { 2  string  string1 =  " two   ;  3  string  string2 =  " one  "  + string1 +  " three  "   4 } 

The runtime must allocate at least one new string type to fit the result. In String.Concat () This is performed efficiently through an external function called fastallocatestring () , but there is no way to bypass the heap allocation (the above example occupies 40 bytes in my system). If you need to dynamically change or concatenate strings, use System.Text.StringBuilder.

Packing

Sometimes the data must move between the stack and the heap. For example, when you format a string like this:

string string. Format ("{0} = {1}"55.0f);

You are calling such a function:

1  Public Static string format (    2string  format,    3params object[] args)

In other words, when you call Format (), the integer 5 and the floating-point number ' 5.0f ' must be converted to System.Object. But object is a reference type and the other two are value types. C # must therefore allocate memory on the heap, copy the values to the heap, and then process the references to the newly created int and float objects in Format (). This process is called boxing, and its reverse process is unpacking.

This behavior may not be a problem for String.Format () because you want it to allocate heap memory (for new strings). But boxing can happen in unexpected places, too. One of the most famous examples is when you want to implement the equals operator "= =" for your own value type (for example, a structure that represents a complex number). Read about the example point here if you avoid implicit boxing .

Library functions

To end this post, I want to say that many library functions also contain implicit memory allocations. The best way to find them is through analysis. Some of the two interesting examples that have been recently encountered are:

    • I mentioned before that the Foreach loop passes through most of the standard generic collection types and does not cause heap allocations. This to Dictionary<k, V> also set up. However, the magic is, Dictionary<k, v> collection and dictionary<k, V> The value collection is a class type, not a struct. means "(K key in Mydict.keys) ..." takes 16 bytes. That's disgusting!
    • List<t>. Reverse () uses a standard in-situ array rollover algorithm. If you are like me, you would think that means that the heap memory is not allocated. Wrong again, at least in Mono2.6. There is an extension method you can use, but it is not as optimized as the. Net/mono version, but it avoids heap allocations. And the use of list<t>. Use it like Reverse ():
 Public Static classlistextensions{ Public Static voidReverse_noheapalloc<t> ( ThisList<t>list) {            intCount =list.            Count;  for(inti =0; I < count/2; i++) {T tmp=List[i]; List[i]= List[count-i-1]; List[countI1] =tmp; }    }}                    

There are other memory traps that can be written. But, I do not want to give you more fish, but teach you to fish. This is the content of the next post!

C # Memory management for Unity developers (in the previous article)

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.