开启辅助访问
 找回密码
 注册帐号

扫一扫,访问微社区

项目源码

关注:1161

当前位置:游戏蛮牛 资源专区 项目源码

查看: 1192|回复: 16

[Unity] Unity C#内存和性能优化技巧

[复制链接]  [移动端链接]
排名
35485
昨日变化
62

2

主题

2

帖子

9

积分

Rank: 1

UID
185535
好友
0
蛮牛币
22
威望
0
注册时间
2016-11-21
在线时间
1 小时
最后登录
2016-12-12
发表于 2016-11-21 16:10:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册帐号

x
  游戏开发要学习的第一件事就是不分配不必要的内存。这样做有很充分的理由。第一,内存是一种有限资源,尤其是在移动设备上。第二,分配内存需要消耗CPU周期(在堆上分配和回收都消耗CPU周期)。第三,在C或C++中手动管理内存,每次分配内存都是引入Bug的契机,Bug会引起严重问题,任何地方的内存泄露都会引起崩溃。
  Unity使用.Net或者可以说是一个开源替代品Mono。它的自动内存管理解决了大量安全问题,例如,不能在内存被释放后再使用(忽略了不安全代码)。但是,分配和释放内存变得更加难以预测。
  假设你已经很了解栈分配和堆分配的区别。简而言之,堆栈数据生存周期比较短,分配/释放几乎不会消耗CPU。堆数据生命周期比较长,分配/释放消耗多些,因为,内存管理器需要跟踪内存分配。在.Net和Mono种,堆内存通过垃圾回收器自动获取。实际上,可以说是个黑盒,用户无法对其进行很多控制。
  .Net的两种数据类型分配方式不同。实例的引用类型总是在堆上分配,然后被GC回收,例如,类、数组(如int[])。数据的值类型在堆栈分配,除非他们的容器已经在堆上(如数组结构),例如基本类型(int,char等)或者结构体实例。最后,值类型可以通过传递引用而从堆栈数据变成堆数据。
  好了,开场结束。让我们谈谈垃圾回收和Mono。
  罪过
  找到并回收堆上数据是GC的工作,不同的回收器在性能上差异很大。
  旧的垃圾回收器因为会产生帧率问题而臭名昭著。例如,一个简单的标记-清除回收器(阻塞回收器),会暂停整个程序,以便一次处理整个堆。暂停时间取决于程序分配的数据数量,如果暂停时间很长,会产生长时间无响应。
  新的垃圾回收器在减少回收暂停方面有不同的方法,例如,现代GC通过在同一位置对所有最近分配进行分组,这样就可以扫描并快速收集被拆分的小块。因为,很多程序喜欢分配可以快速使用和丢弃的临时对象,将它们放在一起管理,有助于GC更快的响应。
  不幸的是,Unity并不支持这些功能。Unity使用的是Mono 2.6.5版本,其GC是旧版的Boehm GC,不属于现代GC。我相信,也不支持多线程。最新版本的Mono已经有了更好的垃圾回收器,然而,Unity并没有升级。反之,他们正在计划使用其它方法来替代。
  虽然这听起来像是一个令人兴奋的改进,但现在我们不得不忍受Mono 2.x 和旧的GC一段时间。
  换句话说,我们需要最小化内存分配。
  机会
  每个人的首要建议都是使用单元数组时用for循环取代foreach循环。这很令人惊讶,foreach循环是代码更加可读,为什么我们要摆脱foreach呢?
  原因是foreach循环在内部创建了一个新的枚举实例,foreach循环用伪代码表示如下:
  foreach (var element in collection) { … }
  编译之后如下:
  var enumerator = collection.GetEnumerator();
  while (enumerator.MoveNext()) {
  var element = enumerator.Current;
  // the body of the foreach loop
  }
  这有下面几个后果:
  1. 使用枚举意味着需要额外的函数调用来遍历集合
  2. 另外:Unity附带的Mono C#编译器有个Bug,foreach循环在堆上抛出一个对象,以至于GC在之后才会清理 (更多细节见this discussion thread)。
  3. 编译器不会尝试把foreach循环优化成for循环,即使是简单的List集合(除了一个特殊优化,就是Mono把通过数组使用的foreach转化为for循环)。
  让我们比较一下拥有16M元素的List和int[]的for、foreach循环。每种里面都使用一个Linq扩展。
  // const SIZE = 16 * 1024 * 1024;
  // array is an int[] // list is a List
  1a. for (int i = 0; i < SIZE; i++) { x += array; }
  1b. for (int i = 0; i < SIZE; i++) { x += list; }
  2a. foreach (int val in array) { x += val; }
  2b. foreach (int val in list) { x += val; }
  5. x = list.Sum(); // linq extension
  time memory
  1a. for loopover array …. 35 ms …. 0 B
  1b. for loopover list ….. 62 ms …. 0 B
  2a. foreach overarray ….. 35 ms …. 0 B
  2b. foreach overlist . …. 120 ms … 24 B
  3. linq sum() …………271 ms … 24 B
  显然,通过数组大小的for遍历用的时间更少。(通过数组大小的foreach遍历进行了优化,所以,和for遍历时间相同)。
  但是,为什么通过list遍历的for循环要比通过数组遍历慢呢?这是因为访问list元素需要通过函数调用,因此,比数组访问要慢一些。如果,我们通过ILSpy这种工具查看这些循环的IL代码,我们可以看见“x += list”已经被编译为“x += list.get_Item(i)”的函数调用。
  Linq Sum()扩展最慢,查看其IL代码,Sum()主体本质上是一个foreach循环,看起来像“tmp = enum.get_Current();x = fn.Invoke(x, tmp)”,其中fn是一个加法函数的委托实例。难怪会比for循环慢一些。
  现在我们看看其它方面的比较。这次二维数组的大小是4K,list也是4K。分别使用for循环和foreach循环,结果如下:
  time memory
  1a. for loops over array[][] …… 35 ms ….. 0 B
  1b. for loops over list> . 60 ms ….. 0 B
  2a. foreach on array[][] ……….. 35 ms ….. 0 B
  2b. foreach on list> …. 120 ms …. 96 KB <– !
  不出意外的话,结果和上一次差不多,但是,这里重要的是foreach循环浪费了多少内存:(1 + 4026)x 24 byteseach ~= 96 KB。想象一下,如果你在每一帧都使用这样的循环的话,会浪费多少内存!
  最后,在紧凑循环或循环遍历大的集合时,数组比其它通用集合性能更好,for循环比foreach循环性能好(执行时间,浪费内存方面)。
  我们可以通过降级为数组来改进性能,更别提内存分配上的改善。
  除了循环和大型集合,其它数据结构并没有太多差别(foreach循环和普通集合简化了编程逻辑)。
  这些数据是怎么得到的
  一旦我们开始查找,我们可以在各种奇怪地方发现内存分配。例如,调用具有可变参数的函数,实际上会在堆上分配一个临时数组来存放这些参数(有C开发背景的人会感到一些意外)。让我们看看操作一个256K的循环体,返回最大数字:
  1. Math.Max(a, b) ……… 0.6 ms ….. 0 B
  2. Mathf.Max(a, b) …….. 1.1 ms ….. 0 B
  3. Mathf.Max(a, b, b) …… 25 ms … 9.0 MB <– !!!
  传入三个参数调用Max意味着调用的是可变参数的”Mathf.Max(params int[] args)”,每次的函数调用将会在堆上分配36字节(36B * 256K = 9MB)。
  另外一个示例,让我们看看委托。解耦合和抽象时委托非常有用,但是委托有个意外行为:将委托分配给局部变量也会引起装箱操作(堆上传递数据)。甚至是仅仅把委托存储在一个局部变量中也会引起堆分配。
  下面是一个在紧凑循环中进行256K次函数调用的例子。
  protected static int Fn () { return 1; }
  1. for (…) { result += Fn(); }
  2. Func fn = Fn; for (…) { result += fn.Invoke(); }
  3. for (…) { Func fn = Fn; result += fn.Invoke(); }
  1. Static function call ……. 0.1 ms …. 0 B
  2. Assign once, invoke many … 1.0 ms … 52 B
  3. Assign many, invoke many …. 40 ms … 13 MB <– !!!
  在ILSpy中查看代码,每个像 “Funcfn = Fn”这样的局部变量赋值都会在堆上创建一个新的委托类Func 的实例,然后占用的52字节立即会被丢弃,但是,编译器还不够智能到把这些局部变量放到循环体之外以节约内存。
  这让我很焦虑。Lists或者dictionaries委托会是什么样的呢?例如,当执行观察者模式或者一个handler函数的dictionary时,如果通过迭代反复调用每个委托会引起大量混乱的堆分配吗?
  让我们试试通过一个256K大小的List<>迭代并执行委托:
  4. For loop over list of delegates …. 1.5 ms …. 0 B
  5. Foreach over list of delegates ….. 3.0 ms … 24 B
  至少通过循环遍历List委托不会重新装箱委托,可以通过IL确认。
  生活本是如此
  还有很多的随机最小化内存分配的机会,简而言之:
  · UnityAPI有些地方希望用户为属性分配一个数组结构,例如在Mesh组件:
  void Update () {
  // new up Vector2[] and populate it
  Vector2[] uvs = MyHelperFunction();
  mesh.uvs = uvs;
  }
  不幸的是,如之前所述,一个局部值类型数组会引起堆分配,即使Vector2 是值类型,该数组仅仅只是一个局部变量。如果这段代码在每一帧执行,每次创建一个24B新数组,再加上每个元素的大小(假设Vector2每个元素大小为8B)。
  有个修复办法,但是有些不好看:维护一个合适大小的list并重复使用。
  // assume a member variable initialized once:
  // private Vector2[] tmp_uvs;
  void Update () {
  MyHelperFunction(tmp_uvs); // populate
  mesh.uvs = tmp_uvs;
  }
  这很管用,因为Unity API属性设置器将默默地生成一个传入数据的数组副本,而不是引用数组(和想象中有些不同)。所以,始终没有生成临时复制的时间点。
  因为数组不能被重置大小,所以,常常使用List<>添加或者移除元素,例如:
  List ints = new List();
  for (…) { ints.Add(something); }
  作为实现细节,当使用默认构造函数分配List时,List会非常小(即仅仅分配一个只有少量元素的内部存储,例如4)。当超出list大小时,会重新分配一块更大的内存,并将数据复制到新分配内存。
  因此,如果游戏需要创建一个list并加入大量元素,最好像下面这样指定list的容量。甚至可以多分配一点以避免不必要的重置大小和重新分配内存。
  List ints = newList(expectedSize);
  List另一个有趣的副作用是,即使当清除list时,list不会释放分配的内存(例如,容量保存不变)。如果list中有许多元素,调用Clear()时内存也不会被释放,而仅仅只是清除数据内容并设置为0。同样,增加新元素时list也不会分配新的内存,直到容量用完。
  和第一个小技巧相似,如果函数需要在每一帧填入并使用一个大量数据的list,一个猥琐却很有效的优化技巧是,在使用之前预先分配好list,然后维护重用并在每次使用之后清除数据,从而不会引起内存的重新分配。
  最后,简短说明一下字符串。Strings在C#和.Net中是不可变对象。因此,string在堆上生成新的实例。当我们把多个组件的字符串集合一起时,通常最好使用StringBuilder,它拥有内部字符缓冲区可以最终创建一个新的字符串实例。任何实例化代码都是单线程的、不可重入。即使是共享一个静态builder实例,在调用之间重置,那样才可以重用缓冲区。
  值得吗?
  我在收集所有这些优化技巧时受到一些企发,通过挖掘、简化代码摆脱了一些非常烂的内存分配。在特别坏的情况下,仅仅因为使用了错误的数据结构和迭代器,一帧分配了约1MB的临时对象。在移动设备上面缓解内存压力更加重要,因为纹理内存和游戏内存必须共享非常有限的内存池。
  最后,这些技巧并不是一成不变的规则,只是一些优化时机。实际上我非常喜欢使用Linq,foreach和其它有效的扩展,并经常使用。这些优化只在频繁处理数据或者处理大量数据时使用,但是,多数情况下并不必要。
  最终,优化的标准做法是:首先,我们应该写好代码。然后是分析,只有那时再谈优化实际观察到的热点问题。因为,每个优化都牺牲了灵活性。好了,本篇unity3d教程关于Unity C#内存和性能优化技巧到此结束,下篇我们再会!


回复

使用道具 举报

5熟悉之中
975/1000
排名
21542
昨日变化
24

10

主题

563

帖子

975

积分

Rank: 5Rank: 5

UID
158776
好友
1
蛮牛币
15
威望
0
注册时间
2016-7-26
在线时间
391 小时
最后登录
2017-6-27
发表于 2016-11-23 15:02:40 | 显示全部楼层
谢谢楼主分享

回复

使用道具 举报

排名
47575
昨日变化
226

0

主题

6

帖子

8

积分

Rank: 1

UID
186294
好友
0
蛮牛币
2
威望
0
注册时间
2016-11-23
在线时间
1 小时
最后登录
2016-11-25
发表于 2016-11-23 19:14:17 | 显示全部楼层
作为初学者觉得很高端 先保存下来

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1398/1500
排名
939
昨日变化
1

0

主题

177

帖子

1398

积分

Rank: 6Rank: 6Rank: 6

UID
90184
好友
0
蛮牛币
4426
威望
0
注册时间
2015-4-8
在线时间
341 小时
最后登录
2017-6-27
发表于 2016-11-25 20:40:39 | 显示全部楼层


谢谢楼主分享

回复

使用道具 举报

排名
35485
昨日变化
62

0

主题

14

帖子

20

积分

Rank: 1

UID
187593
好友
0
蛮牛币
7
威望
0
注册时间
2016-11-28
在线时间
4 小时
最后登录
2017-3-31
QQ
发表于 2016-11-28 10:54:05 | 显示全部楼层
我擦。。不错

回复

使用道具 举报

2初来乍到
111/150
排名
16653
昨日变化
624

0

主题

69

帖子

111

积分

Rank: 2Rank: 2

UID
183904
好友
0
蛮牛币
149
威望
0
注册时间
2016-11-15
在线时间
18 小时
最后登录
2017-6-26
QQ
发表于 2016-11-28 14:09:43 | 显示全部楼层
得多看几遍才能理解啊

回复 支持 反对

使用道具 举报

6蛮牛粉丝
1245/1500
排名
4841
昨日变化
7

2

主题

776

帖子

1245

积分

Rank: 6Rank: 6Rank: 6

UID
92518
好友
1
蛮牛币
2466
威望
0
注册时间
2015-4-15
在线时间
277 小时
最后登录
2017-4-6
发表于 2016-12-6 10:25:08 | 显示全部楼层
得多看几遍才能理解啊

回复 支持 反对

使用道具 举报

排名
29470
昨日变化
41

0

主题

10

帖子

19

积分

Rank: 1

UID
128640
好友
0
蛮牛币
9
威望
0
注册时间
2015-11-12
在线时间
5 小时
最后登录
2016-12-15
发表于 2016-12-12 17:15:48 | 显示全部楼层
xie 谢谢分享,正是我要的

回复 支持 反对

使用道具 举报

排名
47575
昨日变化
226

1

主题

36

帖子

53

积分

Rank: 2Rank: 2

UID
173993
好友
0
蛮牛币
31
威望
0
注册时间
2016-10-8
在线时间
14 小时
最后登录
2017-6-21
QQ
发表于 2016-12-13 15:22:14 | 显示全部楼层
略高深,暂时用不到这些东西

回复 支持 反对

使用道具 举报

2初来乍到
142/150
排名
18095
昨日变化
21

0

主题

54

帖子

142

积分

Rank: 2Rank: 2

UID
33969
好友
2
蛮牛币
163
威望
0
注册时间
2014-7-13
在线时间
72 小时
最后登录
2016-12-15
QQ
发表于 2016-12-15 15:39:15 | 显示全部楼层
Mark一下

回复

使用道具 举报

3偶尔光临
272/300
排名
6972
昨日变化
10

0

主题

62

帖子

272

积分

Rank: 3Rank: 3Rank: 3

UID
161307
好友
2
蛮牛币
369
威望
0
注册时间
2016-8-9
在线时间
86 小时
最后登录
2017-4-27
发表于 2016-12-24 10:11:14 | 显示全部楼层
看完了。很不错

回复

使用道具 举报

4四处流浪
312/500
排名
8394
昨日变化
12

1

主题

116

帖子

312

积分

Rank: 4

UID
116097
好友
0
蛮牛币
284
威望
0
注册时间
2015-7-31
在线时间
104 小时
最后登录
2017-6-13
发表于 2017-1-2 13:58:04 | 显示全部楼层
666666666666

回复

使用道具 举报

3偶尔光临
265/300
排名
8106
昨日变化
14

0

主题

81

帖子

265

积分

Rank: 3Rank: 3Rank: 3

UID
68740
好友
1
蛮牛币
297
威望
0
注册时间
2015-1-15
在线时间
86 小时
最后登录
2017-6-23
发表于 2017-1-8 00:59:56 | 显示全部楼层
谢谢楼主分享

回复

使用道具 举报

排名
12245
昨日变化
254

3

主题

16

帖子

74

积分

Rank: 2Rank: 2

UID
182123
好友
0
蛮牛币
479
威望
0
注册时间
2016-11-8
在线时间
21 小时
最后登录
2017-6-24
发表于 2017-1-9 13:14:08 | 显示全部楼层
谢谢楼主分享

回复

使用道具 举报

3偶尔光临
261/300
排名
6735
昨日变化
4

1

主题

50

帖子

261

积分

Rank: 3Rank: 3Rank: 3

UID
160067
好友
0
蛮牛币
527
威望
0
注册时间
2016-8-2
在线时间
82 小时
最后登录
2017-6-21
QQ
发表于 2017-2-6 10:04:12 | 显示全部楼层
谢谢大神的指导

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册帐号

本版积分规则

快速回复 返回顶部 返回列表