1. 应尽量减少创建 C# 堆内存对象

建议使用成员变量,或者 Pool 来规避高频创建 C# 堆内存对象的创建。而且堆内存对象创建本身就是个相对较慢的过程。

2. 应为 struct 对象重载所有 object 函数

为了普适性,C#struct 的默认 Equals()GetHashCode()ToString() 都是较慢实现,甚至涉及反射。用户自定义的 struct ,都应重载上述3个函数,手动实现,比如:

public struct NetworkPredictId {
    int m_value;
    public override int GetHashCode() {
        return m_value;
    }
    public override bool Equals(object obj) {
        return obj is NetworkPredictId && this == (NetworkPredictId)obj;
    }
    public override string ToString() {
        return m_value.ToString();
    }
    public static bool operator ==(NetworkPredictId c1, NetworkPredictId c2) {
        return c1.m_value == c2.m_value;
    }
    public static bool operator !=(NetworkPredictId c1, NetworkPredictId c2) {
        return c1.m_value != c2.m_value;
    }
}

3. 如果可能,尽量用 Queue/Stack 来代替 List

一般习惯用 List 来实现数据集合的需求。但好一些情况下,事实上是不需对其进行随机访问,而仅仅是 增加删除 操作。此时,应该使用增删复杂度都是 O(1)Queue 或者 Stack

4. 注意 List 常用接口复杂度

Add() 常为 O(1) 复杂度,但超过 Capacity 时,为 O(n) 复杂度。所以应注意合理地设置容器的初始化 CapacityInsert()O(n) 复杂度。 Remove()O(n) 复杂度。RemoveAt(index)O(n) 复杂度,n=(Count - index)。故建议移除时应优先从尾部移除。当批量移除时,宜使用 RemoveRange 提高移除效率。

/// remove not exsiting items in O(n)
int oldCount = m_items.Count;
int newCount = 0;
Item oneItem;
for(int i = 0; i < oldCount; ++i){
    oneItem = m_items[i];
    if(CheckExisting(oneItem)){
        m_items[newCount] = oneItem;
        ++newCount;
    }
}

m_items.RemoveRange(newCount, removeCount);

5. 应注意容器的初始化 capacity

同理如上条目。另,Capacity 增长时,除了 O(n) 的复杂度,也有 GC 消耗。

6. 减少 Dictionary 的冗余访问

程序员习惯编写这样的代码:

if(myDictionary.Contains(oneKey))
{
    MyValue myValue = myDictionary[oneKey];
   // ...
}

但其可减少冗余的哈希次数,优化为:

MyValue myValue;
if(myDictionary.TryGetValue(oneKey, out myValue))
{
    // ...
}

7. 避免使用 foreach()

因为它会调用 GetEnumerator(),从而在循环的过程中在堆中产生 enumerator 对象,而这些对象并无他用,所以应当使用传统的 for 函数来完成工作以避免额外的内存负担。

8. 避免使用 strings

.NETstrings 是不可变长并且是在堆中申请的。而且不能像在 C 语言中的方式一样去修改它。对于 UI,使用 StringBuilder 来创建 strings 是一种比较有效率的做法,而且应当在尽量晚的时候才进行转换。这并不影响你使用作为关键字索引来使用,因为游标是会找到内存中的实例所在,但是请避免过多的修改它。

9. 使用 structs

monostructs 是在栈中申请的。如果有一个工具类同时仅仅是在局部范围使用的,那么就可以考虑把它变成 struct。要记住 structs 是传值的,所以需要通过引用来避免额外的拷贝开销。

10. 使用 structs 来代替运行范围内的固定数组

如果类的方法中有一些 固定大小的数组 ,那么不妨使用 structs 或者 成员数组 来代替它,这样不必每次运行函数都申请变量,尤其是如果这些方法需要被调用成百上千遍。 把引用 List 作为参数传入而不是新创建一个 : 仿佛没有节约什么,因为传入的 List 同样需要申请内存对吗?但这么做的原因是为了下一个看上去并不漂亮的优化。

11. 使用成员变量来代替高频率出现的局部变量

如果函数每次运行时都需要一个很大的 List,那么不妨将这个 List 设为成员变量,这样 List 可以独立于函数运行,在需要时你可以通过 Clear() 函数来清空 List,而在 C# 中 Clear() 并不会真正的将内存空间删除,虽然会有碍于代码的美观,但是可以极大减轻性能上的负担。

12. 避免 IEnumerable 扩展函数

无可厚非 IEnumerable 扩展函数虽然使用方便,但是却会带来更多的内存负担。所以和 foreach() 同样道理,应当尽力避免在代码中使用 IEnumerable<> 接口,可以用 IList<> 来代替。

13. 减少使用函数指针 / delegate

使用 委托 或者 Func<> 会为程序带来新的内存分配,可是实际上却也找不到更好的办法来实现同样的功能,尤其是它会为程序在解耦带来巨大的好处,但是总而言之要尽量精简。

14. 要注意 cloned 材质

如果想获取 renderermaterial 属性,那么需要注意即使不想修改任何东西,系统也会复制一份 material ,而且这个 material 不是自动回收的,只有在切换场景或者手动调用 Resources.UnloadUnusedAssets() 才会被释放。如果你不需要修改材质,通过 myRenderer.sharedMaterial 来访问材质吧。

15. 避免频繁地 SetActive 操作

避免频繁地 SetActive 操作,由于 SetActive 本身也有一定消耗,而且一些特殊的组件类似于:TextMaskGraphic 等,在 OnEnableOnDisable 时有较为明显的消耗,建议在频繁进行 SetActive 的操作时采用先移出屏幕等待一段时间之后再将物体隐藏,保证不过度频繁地将物体重复 Active 或者 Inactive 。而在一些不适用于移出屏幕的物体,类似于 UI,考虑减少该类操作,或者使用将 Text设为空 或者 透明度设为0 来避免调用 OnEnableOnDisable 操作。

16. 使用 gameObject.CompareTag(“XXX”) 而非 gameObject.tag

函数调用 gameObject.tag 会造成预想不到的堆内存分配,这个函数会将结果存为新的字符串返回,这就会造成不必要的内存垃圾,对结果进行缓存是一种有效的办法,但是在 Unity 中都对应的有相关的函数来替代。对于比较 gameObject.tag ,可以采用 GameObject.CompareTag() 来替代。

17. 多使用内建的常量

使用内建的常量,例如 Vector3.zeroVector3.up 等等,避免频繁创建相同的对象。

18. 对于一些简单的协程,建议用自己的工具类实现

自己可以写一个工具类,使用 Update 替代简单的协程,例如等待若干秒等等,可以消除创建协程的消耗。

19. 在UI中使用池时考虑使用分级机制

在池中越不频繁出现的 UI 就应该更快地被销毁以释放内存,而频繁出现的 UI 则等待更长的时间。

20. 在使用池时需要注意的事项

在使用池进行内存管理时特别要注意,当一个物体你不再需要的时候,请将其置为 null 。例如你封装了一个数组,其中装入了许多的对象,当你移除一个对象的时候或许并没有将其真正置空,而是移动了目前指向的位置,那么你本应移除的对象就泄漏了出去。

21. 不要主动调用 GC

不要主动调用 GC。而是通过良好的代码,及时去除不需要的对象的引用可以更好地让我们使用 GC 来回收垃圾。

22. 使用消耗更小的运算

使用消耗更小的运算:例如 1/5 使用 1*0.2 来代替、用 位运算 代替 简单乘除 。(不过在性能并不是非常非常敏感的地方可以忽略位运算这一条,毕竟可读性还是要的。)

23. 使用延迟加载的方式

使用 延迟加载 的方式,一些不常用的资源在第一次使用的时候再进行加载。

24. 垃圾最小化

垃圾回收器 对内存管理表现的非常出色,并且它以非常高效的方法移除不再使用的对象。但不管怎样,申请和释放一个基于堆内存的对象总比申请和释放一个不基于堆内存的对象要花上更多的处理器时间。所以,要避免创建大量的对象,也不要创建你不使用的对象。同时避免在局部函数上多次创建引用对象。相反,把局部变量提供为类型成员变量,或者把你最常用的对象实例创建为静态对象。最后,考虑使用可变对象创建器来构造恒定对象。

25 装箱和拆箱的最小化

.Net 框架使用了 装箱拆箱 来链接两种不同类型的数据。装箱和拆箱可以让你在须要使用 System.Object 对象的地方使用 值类型 数据。但装箱与拆箱操作却是性能的强盗,在些时候装箱与拆箱会产生一些临时对象,它会导致程序存在一些隐藏的 BUG。尽量使用 泛型容器 避免不必要的装箱拆箱。

26. 避免返回内部类对象的引用

所谓的只读属性就是指调用者无法修改这个属性。不幸的是,这并不是一直有效的。如果创建了一个属性,它返回一个引用类型,那么调用者就可以访问这个对象的公共成员,也包括修改这些属性的状态。所以要避免返回内部类对象的引用。

27. 避免转换操作

当你为某个类型添加转换操作时,就等于是告诉编译器:你的类型可以被目标类所取代。这可能会引发一些潜在的错误,因为你的类型很可能并不能被目标类型所取代。它的副作用就是修改了目标类型的状态后可能对原类型根本无效。更糟糕的是,如果你的转换产生了临时对象,那么副作用就是你直接修改了临时对象,而且它会永久丢失在垃圾回收器中。总之,使用转换操作应该基于编译时的类型对象,而不是运行时的类型对象。

28. 选择小而简单的函数

.Net 运行时调用 JIT 编译器,用来把由 C# 编译器生成的 IL 指令编译成机器代码。JIT 编译器是在需要时,以每个函数为单元生成机器指令(当内联调用时,或者是一组方法)。小函数可以让它非常容易被 JIT 编译器分期处理。小函数更有可能成为内联候选对象。当然并不是足够小才行:简单的控制流程也是很重要的。函数内简单的控制分支可以让 JIT 以容易的寄存变量。

29. 选择小而内聚的程序集

把所有的东西,都放到一个程序集,这不利于重用其中的组件,也不利于系统中小部份的更新。很多以二进制组件形式存在的小程序集可以让这些都变得简单。

30. 减少 UnityEngine.Object 的 null 比较

因为 Unity overwrite 了 Object.Equals()unityEngineObject==null 事实上和 GetComponent() 的消耗类似,都涉及到 Engine 层面的机制调用,所以 UnityEngine.Objectnull 比较,都会有少许的性能消耗。对于 基础功能调用栈叶子节点逻辑高频功能,我们应少 null 比较,使用 assertion 来处理。只有在 调用栈根节点逻辑 ,有必要的时候,才进行 null 比较。

31. 减少 GetComponent() 的频率

即使在 Unity5.5 中,GetComponent() 会有一定的 GC 产生,有少量的 CPU 消耗。如有可能,我们依然需要规避冗余的 GetComponent() 。另,自 Unity5 起,Unity 已就 .transform 进行了 cache ,我们不需再为 .transform 担心。

32. 减少每帧 Material.GetXX() / Material.SetXX() 的次数

每次调用 Material.GetXX()Material.SetXX() 都会有消耗,应减少调用该 API 的频率。比如使用 C# 对象变量来记录 Material 的变量状态,从而规避 Material.GetXX() ;在 Shader 里把多个 uniform half 变量合并为 uniform half 4 ,从而把 4 个 Material.SetXX() 调用合并为 1 个 Material.SetXX()

33. 使用支持 Conditional 的日志输出机制

简单使用 Debug.Log(ToString() + "hello " + "world"); ,其实参会造成 CPU 消耗及 GC 。使用支持 Conditional 的日志输出机制,则无此问题,只需在构建时,取消对应的编译参数即可。

34. 减少不必要的 Transform.position / rotation 等访问

每次访问 Transform.position / rotation 都有相应的消耗,应能 cachecache 其返回结果。可以在 start 函数中预先存起来。

35. 涉及物理运算时使用 Layer 而不是 Tag

我们可以轻松为对象分配 层次标签 ,并查询特定对象,但是涉及碰撞逻辑时,层次至少在运行表现上会更有明显优势。更快的物理计算更少的无用分配内存 是使用层次的基本原因。

36. 删除空的 Update 方法

当通过 Assets 目录创建新的脚本时,脚本里会包括一个 Update 方法,当你不使用时删除它。

37. 不要滥用 switch

为你的设计找一个平衡点,不要滥用 switch (包括数组容器或者子类设计)导致复杂度的上升,同时,减少嵌入回调等代码的可能,滥用回调,有可能导致设计框架崩溃,需要重新设计或者项目崩溃。

38. 过大的运算请分帧处理

如果你的一次运算量过大,并且不要求在当前帧完成,请分帧进行处理,对每一帧进行迭代运算,不要把所有运算放在同一帧(例如,寻路算法,遗传算法等等)。

39. 减少函数引用

函数的引用,无论是指向匿名函数还是显式函数,在 unity 中都是 引用类型变量 ,这都会在堆内存上进行分配。匿名函数的调用完成后都会增加内存的使用和堆内存的分配。具体函数的引用和终止都取决于操作平台和编译器设置,但是如果想减少 GC 最好减少函数的引用。

40. 重构代码来减小 GC 的影响

即使我们减小了代码在堆内存上的分配操作,代码也会增加 GC 的工作量。最常见的增加 GC 工作量的方式是让其检查它不必检查的对象。通过重构代码,我们可以返回实体的标记,而不是实体本身,这样就没有多余的 object 引用,从而减少 GC 的工作量。

41. GetType() 会产生 GC Alloc

GetType() 会产生 GC Alloc (每个调用 20 Bytes)。

42. 在针对 GC Alloc 做优化时,对象数量 > 引用关系复杂度 > 对象尺寸

Boehm garbage collector 而言,对象数量直接影响单次 GC 的时间开销 每个对象 90 个时钟周期左右 (大量时间是 cache-missing 所致) 算下来每秒 15M 数目的对象,也就是每毫秒标记 15000 个左右。

43. 使用整数句柄来代替引用

当可以使用 整数句柄 来代替引用时,尽量使用整数句柄 (简化引用关系)。

44. 避免频繁调用分配内存的 accessors

避免频繁调用分配内存的 accessors (如 .vertices/ .normals/ .uvs/ .bones )。

45. 避免频繁调用 Int.ToString() 及其它类型的衍生

避免频繁调用 Int.ToString() 及其它类型的衍生。

46. 避免频繁使用 Mathf.Max 等函数的数组版

避免频繁使用 Mathf.Max 等函数的数组版(多于两个参数都会调到数组版),所有具有 params 修饰的函数都应避免频繁使用(以避免临时数组的分配)。

47. 在不需要时避免使用 GUILayout - OnGUI 时把 useGUILayout 关掉

在不需要时避免使用 GUILayoutOnGUI 时把 useGUILayout 关掉。

48. 在使用协程 yield 时尽量复用 WaitXXX 对象

在使用协程 yield 时尽量复用 WaitXXX 对象 (使用 Yielders.cs ) 而不是每次分配。

49. 避免在Update() 内 FindObjectsOfType()

避免在 Update()FindObjectsOfType()

50. 避免在 Update() 里赋值给栈上的数组

避免在 Update() 里赋值给栈上的数组,会触发堆内的反复分配。

最后修改:2021 年 11 月 08 日 03 : 36 PM
如果觉得我的文章对你有用,请随意赞赏