uGUI Text富文本的顶点数优化的优化

来源:互联网 发布:java技术 编辑:程序博客网 时间:2024/06/05 08:22
在写 uGUI Text富文本的顶点数优化 的时候正好看到这篇文档,
https://unity3d.com/cn/learn/tutorials/topics/performance-optimization/optimizing-garbage-collection-unity-games
于是想测试下自己写的顶点数优化组件对GC的影响,不测不知道,一测吓一跳

将下面的富文本复制30份填入一个Text中,在Text下挂上UIVertexOptimize组件
<size=30><b><color=#000000ff>1</color></b></size>
打开Profiler工具,运行一看,每次Text更新,OptimizeVert函数都会产生高达12M多的堆分配



展开可以看到最主要的堆分配是在

可以看出在调用Linq下的去重扩展函数Distinct时会进行元素比较,由于比较的元素类型Triangle是值类型,默认的值类型比较器会有装箱的操作,所以导致了大量的堆分配
扩展函数Distinct还有个带比较器参数的重载版本,于是创建一个Triangle类型,的比较器

class TriangleCompare : IEqualityComparer<Triangle>
    {
        public bool Equals(Triangle x, Triangle y)
        {
            return UIVertexEquals(x.v1, y.v1) && UIVertexEquals(x.v2, y.v2) && UIVertexEquals(x.v3, y.v3);
        }

        public int GetHashCode(Triangle obj)
        {
            return GetUIVertexHashCode(obj.v1)
                   ^ GetUIVertexHashCode(obj.v2)
                   ^ GetUIVertexHashCode(obj.v3);
        }

        int GetUIVertexHashCode(UIVertex vertex)
        {
            return vertex.color.a.GetHashCode()
                ^ vertex.color.b.GetHashCode()
                ^ vertex.color.g.GetHashCode()
                ^ vertex.color.r.GetHashCode()
                   ^ vertex.normal.GetHashCode()
                   ^ vertex.position.GetHashCode()
                   ^ vertex.tangent.GetHashCode()
                   ^ vertex.uv0.GetHashCode()
                   ^ vertex.uv1.GetHashCode();
        }

        bool UIVertexEquals(UIVertex x, UIVertex y)
        {
            return x.color.a == y.color.a
                   && x.color.b == y.color.b
                   && x.color.g == y.color.g
                   && x.color.r == y.color.r
                   && x.normal == y.normal
                   && x.position == y.position
                   && x.tangent == y.tangent
                   && x.uv1 == y.uv1
                   && x.uv0 == y.uv0;
        }
    }

去重函数改为调用带参版本

vertices = tris.Distinct(new TriangleCompare()).SelectMany(tri =>
            new[]{
                tri.v1,
                tri.v2,
                tri.v3
            }).ToList();

重新运行堆分配降为了1M多

1M多还是非常夸张,继续查看堆分配最多的地方


可以看出往List中添加元素时,List会先判断当前的容量是否足够大,如果不够,会将容量扩大为当前的两倍,这个操作会有相应大小的堆分配
这个堆分配是免不了的,不过原先List<Triangle> tris作为局部变量,分配的堆内存在OptimizeVert函数执行完后就变为垃圾内存了,并在下次执行时又重复创建重复扩容,可以将tris变量提升为成员变量以避免不必要的重复堆分配
最终组件修改为

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class UIVertexOptimize : BaseMeshEffect
{
    struct Triangle
    {
        public UIVertex v1;
        public UIVertex v2;
        public UIVertex v3;
    }

    class TriangleCompare : IEqualityComparer<Triangle>
    {
        public bool Equals(Triangle x, Triangle y)
        {
            return UIVertexEquals(x.v1, y.v1) && UIVertexEquals(x.v2, y.v2) && UIVertexEquals(x.v3, y.v3);
        }

        public int GetHashCode(Triangle obj)
        {
            return GetUIVertexHashCode(obj.v1)
                   ^ GetUIVertexHashCode(obj.v2)
                   ^ GetUIVertexHashCode(obj.v3);
        }

        int GetUIVertexHashCode(UIVertex vertex)
        {
            return vertex.color.a.GetHashCode()
                ^ vertex.color.b.GetHashCode()
                ^ vertex.color.g.GetHashCode()
                ^ vertex.color.r.GetHashCode()
                   ^ vertex.normal.GetHashCode()
                   ^ vertex.position.GetHashCode()
                   ^ vertex.tangent.GetHashCode()
                   ^ vertex.uv0.GetHashCode()
                   ^ vertex.uv1.GetHashCode();
        }

        bool UIVertexEquals(UIVertex x, UIVertex y)
        {
            return x.color.a == y.color.a
                   && x.color.b == y.color.b
                   && x.color.g == y.color.g
                   && x.color.r == y.color.r
                   && x.normal == y.normal
                   && x.position == y.position
                   && x.tangent == y.tangent
                   && x.uv1 == y.uv1
                   && x.uv0 == y.uv0;
        }
    }

    List<UIVertex> verts = new List<UIVertex>();
    List<Triangle> tris = new List<Triangle>();

    public override void ModifyMesh(VertexHelper vh)
    {
        vh.GetUIVertexStream(verts);
        Debug.Log(verts.Count);

        OptimizeVert(ref verts);
        Debug.Log(verts.Count);
        vh.Clear();
        vh.AddUIVertexTriangleStream(verts);
    }

    void OptimizeVert(ref List<UIVertex> vertices)
    {
        if (tris.Capacity < vertices.Count / 3)
        {
            tris.Capacity = vertices.Count;
        }
        for (int i = 0; i <= vertices.Count - 3; i += 3)
        {
            tris.Add(new Triangle() { v1 = vertices[i], v2 = vertices[i + 1], v3 = vertices[i + 2] });
        }
        vertices.Clear();

        vertices.AddRange(tris.Distinct(new TriangleCompare()).SelectMany(t => new[]
        {
            t.v1,
            t.v2,
            t.v3
        }));
        tris.Clear();
    }
}

其中List的Clear操作不会影响到List的容量大小
Distinct扩展函数还是会产生可观的堆分配,是因为其使用了临时HashSet容器,跟List一样在容量变化时会有堆分配,所以在一些频繁调用的地方需要实现一个无GC的去重函数
还有两个迭代操作产生的几百B的堆分配,这就是Unity使用的Mono编译器令人诟病已久的foreach问题

总结为三个问题
1.值类型的默认比较会有装箱操作,这个不确定是普遍的问题还是Unity的Mono编译器的问题,在值类型里重写GetHashCodeEquals函数或写个对应的比较器类可解决
2.容器类的扩容问题,特别是临时容器变量,最好实现一个公用的容器对象池避免重复创建容器 ,Linq的扩展函数中可能有大部分会用到临时容器变量,这些临时容器扩容时可能产生巨量的堆分配,并在函数执行完后直接变为垃圾内存,所以在频繁调用的地方最好不用Linq操作
3.foreach迭代操作的问题,Unity5.5版本说明中说已经修复了该问题,未验证
原创粉丝点击