uGUI Text富文字的頂點數優化的優化
阿新 • • 發佈:2019-01-24
於是想測試下自己寫的頂點數優化元件對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版本說明中說已經修復了該問題,未驗證
展開可以看到最主要的堆分配是在
可以看出在呼叫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