在写
uGUI Text富文本的顶点数优化 的时候正好看到这篇文档,
于是想测试下自己写的顶点数优化组件对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编译器的问题,在值类型里重写
GetHashCode和
Equals函数或写个对应的比较器类可解决
2.容器类的扩容问题,特别是临时容器变量,最好实现一个公用的容器对象池避免重复创建容器 ,Linq的扩展函数中可能有大部分会用到临时容器变量,这些临时容器扩容时可能产生巨量的堆分配,并在函数执行完后直接变为垃圾内存,所以在频繁调用的地方最好不用Linq操作
3.foreach迭代操作的问题,Unity5.5版本说明中说已经修复了该问题,未验证