4

子线程GC导致主线程函数耗时较高的问题

 3 years ago
source link: https://blog.uwa4d.com/archives/TechSharing_274.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1)子线程GC导致主线程函数耗时较高的问题
​2)升级Unity大版本后,Text颜色修改问题
3)清除增量式GC导致的Mono堆内存泄漏问题
4)多Pass合批优化问题


这是第274篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题,建议阅读时间10分钟,认真读完必有收获。

UWA 问答社区:answer.uwa4d.com
UWA QQ群2:793972859(原群已满员)

Rendering

Q:在主线程中有非常多的卡顿,从UWA的报告中看到很多异常的高耗时,请问可能是什么原因造成的?

1.png

A:像上面这样的频繁卡顿,且卡顿函数种类非常多的情况,应该是子线程分配了非常多的堆内存,导致子线程GC,从而卡住了主线程。当GC的时候,主线程可能会处于各种阶段,因此对应阶段的函数耗时就会包括等待GC的耗时。可以从UWA的Mono报告中查看是否有子线程分配了大量的堆内存,通常是由这种(Thread)打头的子线程函数的分配导致的。

2.png

感谢han@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/6180e0f28f8c834241fd6930


Q:从Unity 4.6.9f1升级到Unity 2020.3.2.f1c1。首次Unity升级之后UI的Text颜色修改是正常。当运行一次之后,所有的Text颜色都无法修改了。就算新创建一个新的Text也无法修改。

A:会有多余的UI-Default和UI-DefaultFont这两个Shader,删除它们,然后重启Unity就好了。

感谢芝麻青豆角@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/6185085fd8413e18eb034520


Q:最近在研究Mono堆内存时,发现一帧内分配多次较大内存,会导致内存无法被回收。

过程如下:

在同一帧内调用三次分配100MB内存的方法,分配内存的变量都是在各自的作用域,在这之后调用GC.Collect()。发现有较大几率出现100MB或者200MB无法被回收的Mono堆内存。经过排查之后发现取消勾选Incremental GC之后,内存就能完全被收回。

我推测是每次申请内存的时候会执行一次GC,清除上一次分配的内存,但是由于使用增量GC,无法在本帧完成回收工作。再下一次GC的时候,第二次GC的回收内容被重置了,导致第二次的GC需要被回收的内存就泄漏了。

不知道我这么理解是不是对的,希望大佬们解惑。同时希望大佬告知有没有办法清除这部分内存。

测试环境:
Unity 2019.4.15c1
Unity 2020.4.15f2

测试平台:
安卓 Mono

以下为测试代码:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class TestMonoMemory : MonoBehaviour
{
    void Do()
    {
        CallBack01();
        CallBack02();
        CallBack03();
    }

    public void CallBack01()
    {
        List<int> i = new List<int>(1024 * 1024 * 100 / 4);
        //CallBack02();
    }

    public void CallBack02()
    {
        List<int> i = new List<int>(1024 * 1024 * 100 / 4);
        //CallBack03();
    }

    public void CallBack03()
    {
        List<int> i = new List<int>(1024 * 1024 * 100 / 4);
    }

    public string text;

    private void OnGUI()
    {
        GUILayout.Label("Allocated Mono heap size :" + Profiler.GetMonoHeapSizeLong() / (1024 * 1024) + "MB");
        GUILayout.Label("Mono used size :" + Profiler.GetMonoUsedSizeLong() / (1024 * 1024) + "MB");

        GUILayout.Label("Total Reserved memory by Unity: " + Profiler.GetTotalReservedMemoryLong() / (1024 * 1024) + "MB");
        GUILayout.Label("- Allocated memory by Unity: " + Profiler.GetTotalAllocatedMemoryLong() / (1024 * 1024) + "MB");
        GUILayout.Label("- Reserved but not allocated: " + Profiler.GetTotalUnusedReservedMemoryLong() / (1024 * 1024) + "MB");

        if (GUILayout.Button("DO"))
        {
            Do();
        }

        if (GUILayout.Button("DO1"))
        {
            CallBack01();
        }

        if (GUILayout.Button("DO2"))
        {
            CallBack02();
        }

        if (GUILayout.Button("DO3"))
        {
            CallBack03();
        }

        if (GUILayout.Button("GC"))
        {
            System.GC.Collect();
        }
    }
}

A:通过题主的方法,我也复现了该问题。我还是比较赞同你的理解的。

Unity官网的一篇关于增量GC的博客也写了该方法的弊端:

3.png

它的正常运行是需要前提的,那就是在GC期间这些被标记为“需要清理的内存”都保持不变。如果在GC期间变化了,比如频繁分配大量内存,那么就需要重新标记一遍,而这个阶段可能会有意想不到的bug产生。内存泄漏很可能就是发生在这个阶段(我也只是猜测)。

不过个人觉得增量GC仍然是一个良好的尝试,虽然它只是测试阶段,也存在着一些问题,但是以前的Boehm-Demers-Weiser garbage collector很容易引起高耗时峰值从而造成卡顿,而分代式GC正是为了减轻峰值的影响,尽量确保流畅性。

可以参考官方博客:https://blog.unity.com/technology/feature-preview-incremental-garbage-collection

该回答由UWA提供,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/61658ad48f8c834241d92a0b


Rendering

Q:游戏使用简单的Mesh显示几十张扑克牌,为了效果,材质Shader使用了2个Pass,其中一个是拉伸做边缘效果,但是由于是多Pass,即使是相同的材质和贴图,还是不能动态合批,请教这种情况有没有什么优化方案?

1. 不能合批,但是材质是相同的,顺序渲染过程中是否有什么状态切换的消耗?

2. 能否使用两个Mesh,不同的材质,Shader都使用单Pass,区别是一个Mesh是正常显示,一个Mesh仅仅做拉伸边缘效果,理论上是不是不超过顶点数的情况下可以两次动态合批?

A1:Unity BuiltIn渲染管线不支持多Pass合批,多Pass的Shader通过Set Pass call逐个渲染每个Pass后才会继续渲染下一个Object重复一遍,所以会产生比较多的Draw Call。

更好的办法就是用自定义的渲染管线的方式渲染多个Pass。在渲染第一个Pass的时候,把所有的Object一次性全都渲染了,渲染完毕之后通过一次Set Pass Call去渲染下个Pass,可以试下URP渲染管线。

感谢星傲蝶恋@UWA问答社区提供了回答

A2:题主说的第二点是比较常规的做法,要注意的是为了防止Draw Call的穿插,需要调整这两种渲染的RenderQueue,将他们对应的RenderQueue错开。

感谢Xuan@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/6178c9c58f8c834241f2aaeb

20211108
更多精彩问题等你回答~

封面图来源于网络


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK