Unity 开发中常见的 C# 性能问题

for VS. foreach

老版本的 Unity (测试用的是 Unity 5.0.2f1) foreach 是有大概 40B 的 GC,所以,项目重遍历 IList、ArrayList 和 Dictionary 时都不要用 foreach。
使用 for 替代,或者使用如下写法:

  • List 的非 foreach 遍历

    1
    2
    3
    4
    5
    List<int> data = new List<int>();
    var e = data.GetEnumerator();
    while(e.MoveNext())
    {
    }
  • Dictionary 的非 foreach 遍历

    1
    2
    3
    4
    5
    6
    var enumerator = dictionary.GetEnumerator();
    while (enumerator.MoveNext())
    {
    var element = enumerator.Current;
    element.Value.DoSomethine();
    }

    注意:Unity 5.5 已修复此问题。

字符串的拼接

连接次数只有几次(10 以内),此时应该直接用 + 号连接,不产生 GC。实际
上,编译器已经做了优化。

其余的使用 StringBuilder, StringBuilder 内部 Buffer 的缺省值为 16,
按 StringBuilder 的使用场景, Buffer 肯定得重新分配。经验值一般用 256 作
为 Buffer 的初值。当然,如果能计算出最终生成字符串长度的话,则应该按这
个值来设定 Buffer 的初值。使用 new StringBuilder(256) 就将 Buffer 的初始
长度设为了 256。

Struct VS. Class

Struct 在栈中不产生 GC, Class 在堆中,会产生 GC。
对 Struct 的结点修改时,修改完以后记得重新赋值。 因为 Struct 赋值是 copy
而不是引用,修改完以后,以前的不生效。

使用情景:

  • 堆栈的空间有限,对于大量的逻辑的对象,创建类要比创建结构好一些。
  • 结构表示轻量对象,并且结构的成本较低,适合处理大量短暂的对象。
  • 在表现抽象和多级别的对象层次时,类是最好的选择。
  • 大多数情况下该类型只是一些数据,且数据的类型为值类型时,结构是最佳的选择。

数组、ArrayList 和 List 的区别?

  • 数组:内存中是连续存储的,索引速度非常快,赋值与修改元素也很简单。但不利于动态扩展以及移动。因为数组的缺点,就产生了 ArrayList。
  • ArrayList:使用该类时必须进行引用,同时继承了 IList 接口,提供了数据存储和检索,ArrayList 对象的大小动态伸缩,支持不同类型的结点。ArrayList 虽然很完美,但结点类型是 Object,故不是类型安全的,也可能发生装箱和拆箱操作,带来很大的性能耗损。
  • List 是泛型接口,规避了 ArrayList 的两个问题。

Enum

Enum 会产生 GC 的两种情况:

  1. 把枚举当 TKey 使用。也就是以枚举作 Dictionary 的键。
  2. 把枚举转成 string 使用。

把枚举作为 Dictionary 的键会产生 GC,是因为将 enum 值类型装换成 object 引用类型会产生装箱。
Dictionary 本质上,比较键的方法是使用 EqualComparer<T>.Default,然后调用 GetHashCode() 以找到正确的存储桶,并 Equals 比较存储桶中是否有我们要寻找的值。详细解释请看 http://stackoverflow.com/a/26281533

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using UnityEngine;
using System.Collections;
using System;
public class TestClosure : MonoBehaviour
{
void Update ()
{
int num = 1;
Action action = () =>
{
int count = num++;
};
action();
}
}

上面的代码会有 112B 的 GC。

使用闭包的时候,涉及到大约 112B 的 GC,随着闭包引用内容的增加而增大。看自己的需求,如果函数调用频繁的话,要考虑是否不使用闭包来实现。

委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void TestDelegate1(String param)
{
}

public Action<string> TestDelegate2 = (param) =>
{
};

public void TestFunction(Action<string> callback)
{
//do something
callback("this is a test string");
//do something
}

public void TestCall1()
{
TestFunction(TestDelegate1);
}

public void TestCall2()
{
TestFunction(TestDelegate2);
}

TestCall1() 调用有 GC 问题,而 TestCall2() 调用没有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Test : MonoBehaviour
{
void Update ()
{
UnityEngine.Profiling.Profiler.BeginSample("TestDelegate1");
TestCall1();
UnityEngine.Profiling.Profiler.EndSample();

UnityEngine.Profiling.Profiler.BeginSample("TestDelegate2");
TestCall2();
UnityEngine.Profiling.Profiler.EndSample();

UnityEngine.Profiling.Profiler.BeginSample("TestDelegate3");
TestFunction2(TestDelegate3);
UnityEngine.Profiling.Profiler.EndSample();

UnityEngine.Profiling.Profiler.BeginSample("TestDelegate4");
TestFunction2(TestDelegate4);
UnityEngine.Profiling.Profiler.EndSample();
}

public void TestDelegate1(string param)
{
}

public Action<string> TestDelegate2 = (param) =>
{
};

public void TestFunction(Action<string> callback)
{
//do something
callback("this is a test string");
//do something
}

public void TestCall1()
{
TestFunction(TestDelegate1);
}

public void TestCall2()
{
TestFunction(TestDelegate2);
}

public delegate void MyDele(string p);
public void TestDelegate3(string p)
{
}
public MyDele TestDelegate4 = (param) =>
{
};
public void TestFunction2(MyDele callback)
{
//do something
callback("this is a test string");
//do something
}

public void TestCall3()
{
TestFunction2(TestDelegate3);
}

public void TestCall4()
{
TestFunction2(TestDelegate4);
}
}

在 Unity3D 2017.4.0 上测试,TestCall1()TestCall3() 的调用都有 104B 的 GC,而 TestCall2()TestCall4 则没有 GC。