给Unity开发者的C#内存管理(第一部分) C# Memory Management for Unity Developers (part 1 of 3)

来源:互联网 发布:苹果内存清理软件 编辑:程序博客网 时间:2024/05/01 16:35

原文地址:http://www.gamasutra.com/blogs/WendelinReich/20131109/203841/C_Memory_Management_for_Unity_Developers_part_1_of_3.php

前言:查对象池资料的时候看到一篇很好的文章(中文地址:http://www.cnblogs.com/mezero/p/3955130.html) ,是这个系列的第三篇。

我没有找到前两篇的翻译,所以虽然文笔不好,还是尝试将其翻译出来以便阅读,有看不懂的地方可翻阅原文,我会在文章尾部给出原文,如有错误或更好的翻译,请留言

我会尽快上传第二篇 :)



正文:

【这篇文章要求‘中级’C#及Unity知识】

我打算以忏悔来开始这一篇文章:尽管我是从C/C++学起的,但在很长一段时间里我都是微软C#和.NET框架的秘密粉丝。大约三年前,当我决定离开像是西部荒野一样的C/C++图形库,进入游戏引擎的文明世界时,Unity因为一个特性脱颖而出,成为我的首选。Unity不要求你用一个语言(比如Lua 或者 UnrealScript)去写脚本而用另外一个语言去编译。相反,它深度支持Mono,这也就意味着你所有的代码都可以用任意一种.NET语言去边写。这真是爽死了,我终于有了一个正当的理由去和C++说再见,并且让自动内存管理去解决我的所有问题。这个特性被C#写入底层并且是Unity理念的一个部分。再也没有了内存泄漏,再也不用思考内存管理!我的生活变得更加简单。



别把这个给你的玩家看


如果你对Unity或是游戏开发有那么一点点了解,你就会知道我犯了什么错。在游戏开发中,我学到了一个惨痛教训:你不能完全依赖你的自动内存管理。如果你的游戏或是中间件足够的复杂并且需求较多的资源,在Unity中使用C#还不如C++。每一个学习Unity的菜鸟都会迅速发现内存管理是一个棘手问题——你不能简单的把它委托给CLR(Common LanguageRuntime)。Untiy论坛和许多相关博客上有一大堆关于内存的Tips和方法。不幸的是,它们不一定对所有的情况适用,并且在我的记忆中,没有一个是全面讲解的。此外,专业的 C#网站如Stackoverflow对解决Unity开发者一些古怪而不常见的问题没什么经验。因此,在这一篇以及接下来的两篇博文中,我会尝试给出一个全面的介绍以及一些深入的知识——关于C#中的具体Unity内存管理问题


第一篇博文将会讨论在.NET和Mono的垃圾回收机制下内存管理的基础。我也会谈及一些常见的内存泄漏案例。

第二篇着眼于检测内存泄漏的工具。在这方面,Unity Profiler是一个强大的工具,不过它也很贵。所以我想说说.NET的反汇编工具和CIL(CommonIntermediate Language),告诉你怎么只用免费的工具发现内存泄漏。

第三篇博文讨论了C#对象池。焦点又一次来到Unity/C#开发时产生的具体需求上。

我对一些重要的主题已经有想法,想知道的话可以在评论提到,我会回复的。



垃圾回收的限制

绝大多数的现代操作系统将动态内存分成了栈和堆两部分,并且许多CPU架构(包括你PC/MAC、手机和平板电脑里的那一块)在它们的指令设置中支持这一划分。C#通过区别值类型(简单的内置类型和用户通过enum或struct关键字来自定义的类型)和引用类型(类,接口,delegates)来完成这一划分。值类型被分配至栈区,引用类型被分配至堆区。当一个新线程开始的时候,栈区会被设置一个固定的大小。它通常很小,比如说,在Windows上的.NET线程默认的栈区大小为1Mb。这个内存被用来载入线程的主函数以及局部变量,然后不断加载和卸载被主函数调用的函数(以及他们的局部变量)。其中的一些可能被映射到CPU的缓存区中去加速运行。只要调用的深度没有过高或你的局部变量过大,你就不用害怕栈区溢出。在结构化程序设计的概念下,你会发现这种栈区的用法对齐得很好。

如果对象太大以至于超出了栈区的大小,或者生存期比创建它们的函数还长,那就轮到堆区出场了。堆区是“其他的一切”——一部分(通常)能根据操作系统的请求增长并在它期望的程序规则下的内存。但相比栈区微不足道的管理(只使用一个指针来储存哪里是可用片段的开始),堆区的碎片必须以和被分配进去的顺序相反的顺序被释放(即先进后出)。想想看,若把堆区比作一个瑞士奶酪而你必须记住上面所有的洞!这一点都不有趣。

如果使用自动内存管理。关于自动分配的任务——主要是持续追踪奶酪上所有的洞——就变得更加简单,并且被几乎所有的现代编程语言支持。更难的是自动释放,尤其是选择一个对象何时准备被释放,因此你不必这么做。

后一个任务被称为垃圾收集。虽然当一个对象的内存可以被释放时你通知了你的运行环境,但运行时(Runtime)持续追踪这个对象所有的引用,从而能够确认——在一定的间隔后——什么时候一个对象再也不能被你的代码使用了。然后这样一个对象便可以被摧毁并且使它的内存得到释放。GC仍然被专家们积极研究,所以GC在.NET框架下的 1.0版本以后结构得到改变并显著增强。然而,Unity使用的并不是.NET而是它的开源兄弟Mono,在这一点上Unity持续落后于竞争对手。此外,Unity默认使用的并不是Mono的最新版本(2.11/3.0),而仍然使用了2.6版本(切确的说,在我电脑上安装的Unity4.2.2使用的是2.6.5版本[编辑:Unity4.3也没有改变])如果你不知道自己确定的方法,我将会在下一个博文中谈到它。

在2.6版本以后,Mono最主要的更新之一就是GC。新版本使用的是generation GC,然而2.6仍然使用更简单的Boehm GC。更先进的generation GC表现的如此之好以至于它甚至能被用于(在一些限制下)实时应用,例如游戏。Boehm GC,在另一方面,通过在一个相对‘罕见’的间隔下在堆上作详尽的垃圾搜索来工作(比如,以通常比游戏中一帧一次的频率更加低的频率)。因此它不可避免的会在一定的间隔下跳过一些帧,从而惹恼你的玩家。Unity文档推荐你在游戏进入一段帧数影响不大的地方(例如,载入新关卡或显示菜单)时调用System.GC.Collect() 。然而,对于很多游戏来说,这样的计划太少出现了,也就意味着GC也许会在你不想让它出现时提前出现。如果遇到这种情况,你只能咬紧牙关,自己管理内存。这就是这篇文章剩下的部分以及之后的两篇博文要讲的。


成为你自己的内存管理员

让我们来弄明白在Unity/.NET中的“自己管理内存”意味着什么吧。你影响内存分配的能力(非常不幸地)非常有限。你可以选择你习惯的数据结构是类(通常被分配到堆区)还是结构体(通常被分配到栈区,除非它们包含了一个类)。如果你想要更多神奇的力量,你必须使用C#的不安全关键字。但是不安全的代码是无法验证的代码,也就意味着它无法在Unity WebPlayer和一些其他平台上运行。因为各种原因,别使用不安全的代码。因为上面提到的栈区的限制,以及C#的数组仅仅是System.Array(一个类)的语法糖,你不能并且也不应该避免自动的堆分配。你应该避免的是不必要的堆分配,我们会在下一个(以及最后一个)博文中提到。当你打算释放它时,你同样受到限制。事实上,唯一一种可以释放堆对象的途径就是GC,并且它的工作不受你的影响。你所能影响的就是你在堆中的任意一个对象的引用何时会超出范围,因为GC在这以前无法接触到他们。这个受限制的力量事实上有着巨大的现实意义,因为周期GC(你无法阻止)在没有东西去释放时会非常快速的执行。这一事实是许多实现对象池的方法的基础,我将会在第三篇探讨。



不必要的堆分配的常见原因


应该避免foreach循环吗?

一个普遍的建议是:使用for或while来代替foreach。原因很好解释,foreach仅仅是一个语法糖,因为编译器会把下面的代码:

foreach (SomeType sin someList)

   s.DoSomething();

处理成这下面的代码:

using (SomeType.Enumerator enumerator =this.someList.GetEnumerator())

{

   while (enumerator.MoveNext())

   {

       SomeType s =(SomeType)enumerator.Current;

       s.DoSomething();

   }

}


换句话说,每一次使用foreach实际上都会创建一个枚举器对象——一个System.Collections.IEnumerator 接口的实例。但它是在栈区还是堆区创建的这个对象呢?答案是都有可能!这样就产生了一个神奇的问题。最重要的是,几乎所有的在System.Collections.Generic命名空间中的collection类型(例如,List<T>, Dictionary<K, V>, LinkedList<T>)都能智能的通过它们的GetEnumertator()的实现返回一个结构体。现在Unity使用的Mono2.6.5也符合这种情况。

[编辑]Matthew Hanlon使我注意到Microsoft现在的C#编译器和更旧的Mono/C#编译器之间的不幸(也很有趣)的差异,那就是Unity使用被称为‘隐藏在底层’的机制来动态编译你的代码。你也许知道你能用Micros的Visual Studio来写甚至是编译Unity/Mono兼容的代码。你仅仅需要拖动对应的组件进入‘Assets’文件夹。接着所有的代码就会在Unity/Mono运行环境中被执行。然而,根据编译器的不同,结果可能也会改变。Foreach循环就是一个例子——这也是我目前仅知道的唯一一个。所有的编译器都能明白collection的GetEnumerator()返回的是一个结构体还是一个类,然而Mono/C#有一个bug,那就是打包(在接下来的打包部分会提及)一个结构体枚举器时会创建一个引用类型。

所以说我们应该避免使用foreach循环吗?

·       在你允许Unity去编译的代码中不要使用他们

·       当你使用现有的编译器去自主编译的C#代码里需要遍历标准的genericcollections(例如List<T>)时,可以使用它们。Visual Studio和免费的.NET框架的也可以,而且我猜想(不过没有去证实)一个最新版本的Mono和MonoDevelop应该也可以。


应该避免闭包以及LINQ吗?

你也许知道C#提供了匿名方法以及lambda表达式(它们两个几乎是完全相同的)。你能使用delegate关键字以及=>操作符来分别创建它们。它们一般情况下是方便的工具,而且当你使用一些库函数(例如List<T>.Sort())或者LINQ时,使用它们几乎是不可避免的。

使用匿名方法以及lambdas会导致内存泄漏吗?答案是:这取决于你怎么看待它。C#编译器有两种非常不同的方式去处理他们。为了了解差异,请思考一下下面这段代码:

int result =0;

   

void Update()

{

   for (int i =0; i <100;i++)

    {

       System.Func<int,int> myFunc = (p) => p * p;

       result += myFunc(i);

   }

}

如你所见,这个片段看起来是创建了一个myFunc委托一百次,然后每一次都通过调用它来完成计算。但是Mono只会在Update()方法第一次被调用的时候分配一次堆内存(在我的系统上是52bytes的大小),在接下来的帧里就不会分配更多的堆内存了。发生了什么呢?使用一个叫做code reflector(代码反射器)(我会在下一篇博文中解释)的东西,你就能发现C#编译器简单的把myFunc替换为一个System.Func<int,int>类型的静态字段,这个静态字段在一个包含了Update()的类中。这个字段有一个奇怪但是意义明显的名字:f__am$cache1(或许在你的系统上会有一些不同)。换句话说,这个委托只被分配了一次,接着就被缓存起来。

现在让我们对委托的定义做一些小小的改动:

       System.Func<int,int> myFunc = (p) => p *i++;

用‘i++’来替代‘p’,我们已经把一个可以被称为是‘局部定义函数’的东西变成了一个真正的闭包。闭包是模块化编程的核心。他们把函数和数据联系在一起——更准确的说,是和从函数外部定义的非局部变量联系在一起。在myFunc这个例子里,‘p’是一个局部变量而‘i’不是,它属于Update()方法。C#编译器现在不得不把myFunc转化成能被获取甚至能被修改的非局部变量。方法是:通过(紧接着)声明一个全新的类来表示myFunc被创建的那个引用环境(referenceenvironment)。我们每一次经过for循环都会生成一个这个类的对象,所以我们一下子就有了巨大的内存泄漏(在我的电脑上大约是2.6kb每帧)


当然,闭包和其他从C#3.0中引用的语言特性的主要原因就是LINQ。如果闭包能导致内存泄漏,那在你的游戏中使用LINQ安全吗?我也许不适合回答这个问题,因为我总是像躲避瘟疫一样避免使用LINQ。一部分原因是因为LINQ显然在一些不支持实时编译的操作系统上不能工作,比如IOS。但从内存的角度来说,无论如何LINQ都是坏消息。下面是一个令人难以置信的基础表达式:

int[] array = {1,2,3, 6, 7, 8 };

 

void Update()

{

   IEnumerable<int>elements =from elementin array

                    orderby elementdescending

                    where element >2

                    select element;

   ...

}


…在我的系统上在每一帧都分配了68Bytes的大小(其中28Bytes是通过Enumerable.OrderByDescending(),而40Bytes是通过Enumerable.Where())!罪魁祸首不是闭包而是IEnumerable的拓展方法:LINQ不得不创建中介数组来到达最终的结果,并且没有一个能在使用完毕后回收它们的系统。这么说吧,我不是LINQ的专家,并且我也不知道它会不会有那么一个组件能让它安全的在实时环境中使用。


协程

如果你通过StartCoroutine()启动一个协程,你就隐式的分配内存给一个Unity的Coroutine类的实例(在我的系统上是21Bytes)以及一个Enumerator(枚举器,16Bytes)。重要的是,但这个协程yield或resumes(重新开始)的时候,不会再次分配内存。所以你要做的就是当游戏运行的时候,避免调用StartCoroutine()以防止内存泄漏。


字符串

C#以及Unity的内存泄漏总是涉及到字符串。从内存的角度看,字符串是很特殊的,因为它们既在堆上分配内存,又是不可改变的。当你像下面一样连接两个字符串时

void Update()

{

   string string1 ="Two";

   string string2 ="One" + string1 +"Three";

}

运行时必须至少分配出至少一个新的字符串来存放结果。在String.Concat()中,通过一个叫FastAllocateString()的外部方法高效地完成这一步骤。然而这没办法避免堆内存分配(这个例子在我的系统上用了40Bytes)。如果你想在运行时修改或创建一个字符串,请使用System.Text.StringBuilder。


打包

有些时候,数据必须在栈和堆之间被移动。例如,当你像下面一样Format一个字符串时:

string result =string.Format("{0} = {1}",5,5.0f);

你正在调用一个签名如下的方法:

publicstaticstring Format(

       string format,

       params Object[] args

)

换句话说,当Format()被调用时,整形数5和浮点数5.0f必须被转换为System.Object类型。然而Object是一个引用类型,另外两个则是这类型。C#就必须在堆上分配内存,把值复制到堆上,然后把新创建的int和float的Object引用传给Format()。这个过程叫打包,相反的过程叫做解包。

这个行为对于String.Format()来说不是一个问题,因为无论如何你都知道它会分配堆内存(为了一个新的字符串)。但是打包也会出现在一些意想不到的地方。当你想要对自定义类型(比如代表一个复杂数字的struct结构体)执行比较运算符“==”时,一个臭名昭著的例子出现了。想要阅读如何在这样的情况中避免隐藏的打包,请点击这里


库函数

为了结束这篇文章,我想要说的是许多库的方法也会隐藏隐式的内存分配。要捕捉它们,最好的方法就是通过profiling。下面会说到两个有趣的例子:

·       我在前面提到过,对于大多数的标准动态分配来说,foreach循环不会导致堆内存分配。对于Dictionary<K,V>来说这是对的,然而,不知道是什么原因,Dictionary<K,V>.KeyCollection以及Dictionary<K,V>.ValueCollection是类而不是结构,这也就意味着"foreach (K key in myDict.Keys)..."分配了16Bytes.真讨厌!

·       List<T>.Reverse()使用了标准的位置阵列翻转算法。如果你像我一样,那你可能会认为这意味着它不会分配堆内存。又一次错了,至少对于Mono2.6是这样。这里是一个拓展方法,它可能不像.NET\Mono版本一样得到很好地优化,但是至少它成功做到了避免堆内存分配。你怎么使用List<T>.Reverse(),就怎样使用它。

publicstaticclass ListExtensions

{

   publicstaticvoid Reverse_NoHeapAlloc<T>(this List<T>list)

   {

       int count = list.Count;

 

       for (int i =0; i < count /2; i++)

       {

            T tmp = list[i];

            list[i] = list[count - i -1];

            list[count - i -1] = tmp;

       }

   }

}

***

还有其他许多的内存陷阱可以写下来,不过授人以鱼不如授人以渔。我会在下一篇博文中讲述。


Part 2
Part 3 

原文:

[Note: Thispost presupposes 'intermediate' knowledge of C# and Unity.]

I'm going tostart this post with a confession. Although raised as a C/C++developer, for a long time I was a secret fan of Microsoft's C#language and the .NET framework. About three years ago, when I decided toleave the Wild West of C/C++-based graphics libraries and enter the civilizedworld of modern game engines, Unity stood out with one feature that madeit an obvious choice for myself. Unity didn't require you to 'script' in onelanguage (like Lua or UnrealScript) and 'program' in another. Instead, it haddeep support for Mono, which meant all your programming could be done in any ofthe .NET languages. Oh joy! I finally had a legitimate reason to kiss C++goodbye and have all my problems solved by automatic memory managment. Thisfeature had been built deeply into theC# language and was an integral part of its philosophy. No more memoryleaks, no more thinking about memory management! My lifewould become so much easier.


Don't show this to your players.

If you haveeven the most casual acquaintance with Unity and/or game programming, you knowhow wrong I was. I learned the hard way that in game developement, youcannot rely on automatic memory managment. If your game or middleware issufficiently complex and resource-demanding, developing with C# in Unity istherefore a bit like walking a few steps backwards in the directionof C++.  Every new Unity developer quickly learns that memorymanagement is a problematic issue that cannot simply be entrusted to theCommon Language Runtime (CLR). The Unity forums and manyUnity-related blogs contain several collections of tips and best practicesconcerning memory. Unfortunately, not all of these are based on solid facts,and to the best of my knowledge, none of them are comprehensive.Furthermore, C# experts on sites such as Stackoverflow oftenseem to have little patience for the quirky, non-standard problems facedby Unity developers. For these reasons, in this and the following two blogposts, I try to give an overview and hopefully some in-depth knowlege on Unity-specific memorymanagement issues in C#.

·       This first post discusses the fundamentals ofmemory management in the garbage-collected world of .NET and Mono. I alsodiscuss some common sources of memory leaks.

·       The second looks at tools fordiscovering memory leaks. The Unity Profiler is a formidable tool for thispurpose, but it's also expensive. I therefore discuss .NET disassemblersand the Common Intermediate Language (CIL) to show you how you can discovermemory leaks with free tools only.

·       The third postdiscusses C# object pooling. Again, the focus is on the specificneeds that arise in Unity/C# development.

I'm sure I've overlooked some importanttopics - mention them in the comments and I may write them up in a post-script.

Limits to garbage collection

Most modernoperating systems divide dynamic memory into stack and heap (12), and many CPU architectures(including the one in your PC/Mac and your smartphone/tablet) support thisdivision in their instruction sets. C# supports it by distinguishing valuetypes (simple built-in types as well as user-defined types that are declaredas enum or struct) and reference types (classes,interfaces and delegates). Value types are allocated on the stack, referencetypes on the heap. The stack has a fixed size which is set at the start ofa new thread. It's usually small - for instance, .NET threads on Windowsdefault to a stack size of1Mb. This memoryis used to load the thread's main function and local variables, andsubsequently load and unload functions (with their local variables) thatare called from the main one. Some of it may be mapped to the CPU's cache tospeed things up. As long as the call depth isn't excessively high or your localvariables huge, you don't have to fear a stack overflow. And you see thatthis usage of the stack aligns nicely with the concept of structured programming.

If objects aretoo big to fit on the stack, or if they have to outlive the function they werecreated in, the heap comes into play. The heap is 'everything else' - asection of memory that can (usually) grow as per request to the OS, and overwhich the program rules as it wishes. But while the stack is almost trivial tomanage (just use a pointer to remember where the free section begins), the heap fragments assoon as the order in which you allocate objects diverges from the order inwhich you deallocate them. Think of the heap as a Swiss cheese where youhave to remember all the holes! Not fun at all. Enter automatic memorymanagement. The task of automated allocation - mainly, keeping track ofall the holes in the cheese for you - is an easy one, and supported byvirtually all modern programming languages. Much harder is the task ofautomatic deallocation, especially deciding when anobject is ready for deallocation, so that you don't have to.

This lattertask is called garbage collection (GC). Instead of you telling yourruntime environment when an object's memory can be freed, the runtime keepstrack of all the references to an object and is thereby able to determine - atcertain intervals - when an object cannot possibly be reached anymore from yourcode. Such an object can then be destroyed and it's memory gets freed. GCis still actively researched by academics, which explains why the GC architecture haschanged and improved significantly in the.NET framework since version 1.0. However, Unity doesn't use .NET but it'sopen-source cousin, Mono, which continues to lag behind it's commercialcounterpart. Furthermore, Unity doesn't default to the latest version(2.11 / 3.0) of Mono, but instead uses version 2.6 (to be precise, 2.6.5on my Windows install of Unity 4.2.2 [EDIT: the same is true for Unity 4.3]).If you are unsure about how to verify this yourself, I will discuss it in thenext blog post.

One of themajor revisions introduced into Mono after version 2.6 concerned GC. Newversions usegenerational GC, whereas 2.6 still uses the lesssophisticated Boehm garbage collector. Moderngenerational GC performs so well that it can even be used (within limits)for real-time applications such as games. Boehm-style GC, on the other hand,works by doing an exhaustive search for garbage on the heap at relatively'rare' intervals (i.e., usually much less frequently than once-per-frame in agame). It therefore has an overwhelming tendency to create drops in frame-rateat certain intervals, thereby annoying your players. The Unity docs recommend that youcall System.GC.Collect() wheneveryour game enters a phase where frames-per-second matter less (e.g., loading anew level or displaying a menu). However, for many types of games suchopportunities occur too rarely, which means that the GC might kick in beforeyou want it too. If this is the case, your only option is to bite the bulletand manage memory yourself. And that's what the remainder of this post andthe following two posts are about!

Becoming your own Memory Manager

Let's be clearabout what it means to 'manage memory yourself' in the Unity / .NET universe.Your power to influence how memory is allocated is(fortunately) very limited. You get to choose whether your custom datastructures are class (alwaysallocated on the heap) or struct (allocated on the stack unless they are contained withina class), and that's it. If you want more magical powers, you must use C#'s unsafekeyword. But unsafe code is just unverifiable code, meaningthat it won't run in the Unity Web Player and probably some other targetplatforms. For this and other reasons,don't use unsafe. Because ofthe above-mentioned limits of the stack, and because C# arrays are justsyntactic sugar for System.Array (which is a class), you cannot and shouldnot avoid automatic heap allocation. What you should avoid are unnecessaryheapallocations, and we'll get to that in the next (and last) section of this post.

Your powers areequally limited when it comes to deallocation. Actually, the onlyprocess that can deallocate heap objects is the GC, and its workings areshielded from you. What you can influence is when the lastreference to any of your objects on the heap goes out of scope, because the GCcannot touch them before that. This limited power turns out to have hugepractical relevance, because periodic garbage collection (which you cannotsuppress) tends to be very fast when there is nothing to deallocate. This factunderlies the various approaches to object pooling that I discuss in the third post.

Common causes of unnecessary heap allocation

Should you avoid foreach loops?

A common suggestion which I've comeacross many times in the Unity forums and elsewhere is to avoidforeach loops and use for or while loops instead. The reasoningbehind this seems sound at first sight. Foreach is really just syntactic sugar,because the compiler will preprocess code such as this:

foreach (SomeType s in someList)

   s.DoSomething();

...into something like the thefollowing:

using (SomeType.Enumerator enumerator =this.someList.GetEnumerator())

{

   while (enumerator.MoveNext())

   {

       SomeType s =(SomeType)enumerator.Current;

       s.DoSomething();

   }

}

In other words,each use of foreach creates an enumerator object- an instance of theSystem.Collections.IEnumerator interface- behind the scenes. But does it create this object on the stack or on theheap? That turns out to be an excellent question, because both are actuallypossible! Most importantly, almost all of the collection types in the System.Collections.Generic namespace(List<T>, Dictionary<K, V>, LinkedList<T>, etc.) are smart enough to return astruct from from their implementation of GetEnumerator()). This includes the version of thecollections that ships with Mono 2.6.5 (as used by Unity).

[EDIT] Matthew Hanlon pointed myattention to an unfortunate (yet also very interesting) discrepancy betweenMicrosoft's current C# compiler and the older Mono/C# compiler that Unity uses'under the hood' to compile your scripts on-the-fly. You probably knowthat you can use Microsoft Visual Studio to develop and even compile Unity/Monocompatible code. You just drop the respective assembly into the 'Assets'folder. All code is then executed in a Unity/Mono runtime environment.However, results can still differ depending on who compiled the code! foreach loops are just such a case, asI've only now figured out. While both compilers recognize whether acollection's GetEnumerator() returns a struct or a class, the Mono/C# has a bug which 'boxes'(see below, on boxing) a struct-enumerator to create a reference type.

So should you avoid foreach loops?

·       Don't use them in C# code that you allow Unity to compile for you.

·       Do use them to iterate over the standard genericcollections (List<T> etc.) in C# code that youcompile yourself with a recent compiler. Visual Studio as well as the free .NETFramework SDK are fine, and I assume (but haven't verified) that the one thatcomes with the latest versions of Mono and MonoDevelop is fineas well.

Whatabout foreach-loops toiterate over other kinds of collections when you use anexternal compiler? Unfortunately, there's is no general answer. Usethe techniques discussed in the second blog postto find out for yourself which collections are safe for foreach. [/EDIT]

Should you avoidclosures and LINQ?

You probablyknow that C# offers anonymous methods and lambdaexpressions (which are almost but not quite identicalto each other). You can create them with the delegate keyword and the => operator, respectively. They areoften a handy tool, and they are hard to avoid if you want to use certain libraryfunctions (such as List<T>.Sort()) or LINQ.

Do anonymousmethods and lambdas cause memory leaks? The answer is: it depends.The C# compiler actually has two very different ways of handling them. Tounderstand the difference, consider the following small chunk of code:

int result =0;

   

void Update()

{

   for (int i =0; i <100;i++)

    {

       System.Func<int,int> myFunc = (p) => p * p;

       result += myFunc(i);

   }

}

As you can see,the snippet seems to create a delegate myFunc 100 times each frame, using iteach time to perform a calculation. But Mono only allocates heap memory thefirst time the Update() method iscalled (52 Bytes on my system), and doesn't do any furtherheap allocations in subsequent frames. What's going on? Using a code reflector(as I'll explain in the next blog post), one can see that the C# compiler simplyreplaces myFunc by astatic field of type System.Func<intint> in theclass that contains Update(). This field gets a name that is weird but alsorevealing: f__am$cache1 (it maydiffer somewhat on you system). In other words, the delegator is allocatedonly once and then cached.

Now let's make a minor change to thedefinition of the delegate:

       System.Func<int,int> myFunc = (p) => p *i++;

By substituting'i++' for 'p', we've turned something that could becalled a 'locally defined function' into a true closure. Closures area pillar of functional programming. They tie functions to data - moreprecisely, to non-local variables that were defined outside of the function. Inthe case of myFunc, 'p' is a local variable but 'i' is non-local, as it belongs to thescope of the Update() method.The C# compiler now has to convert myFunc into something that can access,and even modify, non-local variables. It achieves this by declaring (behind thescenes) an entirely new class that represents the reference environment inwhichmyFunc wascreated. An object of this class is allocated each time we pass through the for-loop, and we suddenly have a hugememory leak (2.6 Kb per frame on my computer).

Of course, thechief reason why closures and other language features where introduced inC# 3.0 is LINQ. If closures can lead to memory leaks, is it safe touse LINQ in your game? I may be the wrong person to ask, as I have alwaysavoided LINQ like the plague. Parts of LINQ apparently will not work onoperating systems that don't support just-in-time compilation, such as iOS. Butfrom a memory aspect, LINQ is bad news anyway. An incredibly basic expressionlike the following:

int[] array = { 1, 2, 3, 6, 7, 8 };

 

void Update()

{

   IEnumerable<int>elements =from elementin array

                    orderby elementdescending

                    where element >2

                    select element;

   ...

}

... allocates 68 Bytes on my system inevery frame (28 via Enumerable.OrderByDescending() and 40 viaEnumerable.Where())! The culprithere isn't even closures but extension methods to IEnumerable: LINQ has to create intermediaryarrays to arrive at the final result, and doesn't have a system in place forrecycling them afterwards. That said, I am not an expert on LINQ and I do notknow if there are components of it that can be used safely within a real-timeenvironment.

Coroutines

If you launch a coroutine via StartCoroutine(),  youimplicitly allocate both an instance of Unity'sCoroutine class (21 Bytes on my system) and anEnumerator (16 Bytes). Importantly, no allocation occurs when the coroutine yield's or resumes, so all you have to do toavoid a memory leak is to limit calls toStartCoroutine() while thegame is running.

Strings

No overview of memory issues in C# andUnity would be complete without mentioning strings. From a memory standpoint,strings are strange because they are both heap-allocated and immutable.When you concatenate two strings (be they variables or string-constants) as in:

void Update()

{

   string string1 ="Two";

   string string2 ="One" + string1 +"Three";

}

... the runtime has to allocate atleast one new string object that contains the result. In String.Concat() this is done efficiently via anexternal method called FastAllocateString(), but there is no way of getting aroundthe heap allocation (40 Bytes on my system in the example above). If you needto modify or concatenate strings at runtime, use System.Text.StringBuilder.

Boxing

Sometimes, data have to be movedbetween the stack and the heap. For example, when you format a string as in:

string result = string.Format("{0} = {1}",5,5.0f);

... you are calling a method with thefollowing signature:

public staticstring Format(

        string format,

        params Object[] args

)

In other words,the integer "5" and the floating-point number "5.0f" haveto be cast to System.Objectwhen Format() is called. But Object is a reference type whereas the othertwo are value types. C# therefore has to allocate memory on the heap, copy thevalues to the heap, and hand Format() a reference to the newly created int and float objects. This process is called boxing,and its counterpart unboxing.

This behaviormay not be a problem with String.Format() because you expect it to allocate heap memory anway (forthe new string). But boxing can also show up at less expected locations. Anotorious example occurs when you want to implement the equalityoperator "==" for your home-made value types (for example, a struct that represents a complex number). Readall about how to avoid hidden boxing in such cases here.

Library methods

To wind up this post, I want to mentionthat various library methods also conceal implicit memory allocations. The bestway to catch them is through profiling. Two interesting cases which I'verecently come across are these:

·       I wrote earlier that a foreach-loop over most of the standardgeneric collections does not result in heap allocations. This holds true for Dictionary<K, V> as well. However, somewhatmysteriously, Dictionary<K, V>.KeyCollection and Dictionary<K, V>.ValueCollection areclasses,not structs, which means that "foreach (K key in myDict.Keys)..."allocates 16 Bytes. Nasty!

·       List<T>.Reverse() uses astandard in-place array reversal algorithm. If you are like me, you'd thinkthat this means that it doesn't allocate heap memory. Wrong again, at leastwith respect to the version that comes with Mono 2.6. Here's an extensionmethod you can use instead which may not be as optimized as the .NET/Monoversion, but at least manages to avoid heap allocation. Use it in the same wayyou would use List<T>.Reverse():

public staticclass ListExtensions

{

   public staticvoid Reverse_NoHeapAlloc<T>(this List<T>list)

   {

       int count = list.Count;

 

       for (int i =0; i < count /2; i++)

       {

            T tmp = list[i];

            list[i] = list[count - i - 1];

            list[count - i - 1] = tmp;

       }

   }

}

***

There are othermemory traps which could be written about. However, I don't want to giveyou any more fish, but teach you how to catch your own fish. Thats what thenext post is about!
0 0
原创粉丝点击