U3D性能优化之GC(Garbage Collection)

本文一共有五个点:①GC何时触发?②GC触发时如何释放?③GC会对性能产生什么影响?④如何减少GC带来的影响?⑤GC优化实战

一、GC何时触发?

①堆分配时堆上的可用内存不足时触发GC

②GC会不时的自动运行(频率因平台而已)

③手动强制调用GC

二、GC触发时如何释放?

①垃圾收集器检索堆上的每个对象

②垃圾收集器搜索所有当前对象引用以确定堆上的对象是否仍在作用域内

③不在作用域内的对象被标记为删除

④删除被标记的对象并将内存返回给堆

GC是个费时的操作,堆上的对象越多,代码中的引用数越多,GC就越费时

三、GC会对性能产生什么影响?

①最明显的问题是GC可能花费相当长的时间来运行,如果堆上有很多对象和大量的对象引用要检查,则检查所有这些对象的过程可能很慢,这可能会导致我们的游戏卡顿或运行缓慢

②另一个问题是GC可能在不合时宜的时刻被触发,如果CPU在我们游戏的性能关键部分已经满负荷了,那此时即使是少量的GC额外开销也可能导致我们的帧速率下降和性能问题

③另一个不太明显的问题是堆碎片,当从堆中分配内存时,会根据必须存储的数据大小从不同大小的块中的可用空间中获取内存,当这些内存块返回到堆时,堆可能分成很多由分配块分隔的小空闲块,这意味着虽然可用内存总量可能很高,但由于碎片化太过严重而无法分配一块连续的大内存块,导致GC被触发或不得不扩大堆大小(堆内存碎片化有两个后果:1.游戏内存大小会远高于实际所需的大小2.GC会被更频繁的触发)

四、如何减少GC带来的影响?

减少GC的时间:组织我们的游戏使其更少的堆分配和更少的对象引用,堆上更少的对象和更少的引用检查意味着当GC触发时,运行时间更少

减少GC的频率:减少堆分配和释放的频率,特别是在性能点,更少的分配和释放意味着更少的触发GC,这也降低了堆碎片的问题

故意触发GC,以避开游戏运行的性能关键点,比如加载场景时:尝试手动触发GC和扩展堆大小以便GC可控并在合适的时候触发,这个方法更难且不可靠,但作为整体内存管理策略的一部分,可以减少GC的影响

五、GC优化实战

①缓存

//优化前
void OnTriggerEnter(Colloder other)
{
    Renderer[] allRenderers = FindObjectsOfType<Renderer>();
    ExampleFunction(allRenderers);
}


//优化后
private Renderer[] allRenderers;

void Start()
{
    //放在Start函数中,就避免了每次碰撞都去执行查找操作
    allRenderers = FindObjectsOfType<Renderer>();
}

void OnTriggerEnter(Colloder other)
{
    ExampleFunction(allRenderers);
}

②只在满足特定条件时GCAlloc

//优化前
void Update()
{
    ExampleCreateGCAllocFunction(transform.position.x);//假设这是个会产生GCAlloc的方法
}


//优化后
private float previousTransformPositionX;

void Update()
{
    float transformPositionX = transform.position.x;
    if(transformPositionX != prevousTransformPositionX)
    {
        //将这个方法放入判断语句中,而不是每帧都去调用,避免频繁产生GCAlloc
        ExampleCreateGCAllocFunction(transformPositionX);
        previousTransformPositionX = transformPositionX;
    }
}

③使用一个计时器来保证产生垃圾内存的函数每秒只被调一次

//优化前
void Update()
{
    ExampleCreateGCAllocFunction();//这是个会产生GCAlloc的方法
}


//优化后
private float timeSinceLastCalled;

private float delay = 1f;

void Update()
{
    timeSinceLastCalled += Time.deltaTime;
    if(timeSinceLastCalled > delay)
    {
        //如果这个方法实现的需求并不需要每帧都执行,那么就可以设置一个执行频率,比如这个示例为1秒1次
        ExampleCreateGCAllocFunction();
        timeSinceLastCalled = 0f;
    }
}

④清空容器,而不是每次都去new

//优化前
void Update()
{
    List myList = new List();
    PopulateList(myList);
}


//优化后
private List myList = new List();

void Update()
{
    myList.Clear();
    PopulateList(myList);
}

⑤使用对象池(Object Pooling),可以避免频繁地执行创建和销毁操作

https://blog.csdn.net/Victor_Li_/article/details/122679153?spm=1001.2014.3001.5501

⑥字符串

//优化前
public Text timerText;
private float timer;

void Update()
{
    timer += Time.deltaTime;
    //string类是不可变类,每一次字符串拼接操作,都会new一个新的"TIME:"用来执行拼接操作
    timerText.text = "TIME:" + timer.ToString();
}


//优化后
public Text timerHeaderText;//组件Text用于显示"TIME:"
public Text timerValueText;//组件Text用于显示时间
private float timer;

void Start()
{
    timerHeaderText.text = "TIME:";
}

void Update()
{
    timerValueText.text = timer.ToString();
}

//除了上面将"TIME:XXX"分为两个组件显示的办法外,还可以使用StringBuffer或StringBuilder

⑦Unity函数调用

//优化前
void ExampleFunction()
{
    for(int i = 0;i < myMesh.normals.Length; i++)
    {
        Vector3 normal = myMesh.normals[i];//每次调用Unity函数,都会返回一个副本
    }
}


//优化后
void ExampleFunction()
{
    Vector3[] meshNormals = myMesh.normals;//只调用一次,避免不必要的拷贝
    for(int i = 0;i < meshNormals.Length; i++)
    {
        Vector3 normal = meshNormals[i];
    }
}
//优化前
private string playerTag = "Player";

void OnTriggerEnter(Collider other)
{
    //other.gameObject.tag会调用Unity的属性访问器,Unity会返回一个副本
    bool isPlayer = other.gameObject.tag == playerTag;
}


//优化后
private string playerTag = "Player";

void OnTriggerEnter(Collider other)
{
    bool isPlayer = other.gameObject.CompareTag(playerTag);//避免了拷贝
}

⑧装箱

//优化前
void ExampleFunction()
{
    int cost = 100;
    string displayString = String.Format("Num:{0}",cost);
    //实际系统是如下操作
    //int cost = 100;
    //object temp_cost = cost;
    //string displayString = String.Format("Num:{0}",cost.ToString());
}


//优化后
void ExampleFunction()
{
    int cost = 100;
    //避免装箱的原则:注意代码中会隐式转换成System.Object的位置
    string displayString = String.Format("Num:{0}",cost.ToString());
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;

public class Test1 : MonoBehaviour
{
    void Start()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        //经测试,TestFun1()耗时1340ms,TestFun2()耗时1208ms
        TestFun1();//TestFun2();
        sw.Stop();
        UnityEngine.Debug.Log(string.Format("total: {0} ms", sw.ElapsedMilliseconds));
    }

    void TestFun1()
    {
        for (int i = 0; i < 1000000; i++)
        {
            int cost = 100;
            string displayString = String.Format("Num:{0}", cost);
        }
    }

    void TestFun2()
    {
        for (int i = 0; i < 1000000; i++)
        {
            int cost = 100;
            string displayString = String.Format("Num:{0}", cost.ToString());
        }
    }
}

⑨协程

//优化前
yield return 0;//Unity内部会将0装箱成一个Object进行返回


//优化后
yield return null;//这个语句和上面的效果等同,但是省去了装箱操作,所以建议用这个语句
//优化前
while(!isComplete)
{
    yield return new WaitForSeconds(1f);
}


//优化后
WaitForSeconds delay = new WaitForSeconds(1f);

while(!isComplete)
{
    yield return delay;
}

⑩手动强制GC

System.GC.Collect();//在游戏非性能点主动调用

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值