Memory optimization help needed.

Hello there !

I am running into some problems with unity's garbage collector and I am hoping someone can help.

I have a simple trailrenderer script based on this (http://wiki.unity3d.com/index.php?title=OptimizedTrailRenderer) that creates a mesh based on the points in a list. But the way it works is causing lots of data to be created and destroyed constantly. The problem is that when you want to create a new mesh you have to specify the vertices in an array and since I am adding vertices frequently I have to create an array each time. This causes the garbage collector to slow the game down quite significantly.

I tried writing a version where I used lists but even if you use a list the mesh still requires you to parse it an array which is created and destroyed, again causing lots of garbage to be collected.

So is this just a limitation of the unity Mesh implementation or can I do something about this ?

Here is my script :
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Trail_scr : MonoBehaviour {
    
    public Material material;
    public float segmentDistance;
    public float lineWidth;

    bool active = false;
    Material instanceMaterial;
    GameObject trailObj;
    Mesh mesh;
    int trailObjCount;

    List<Vector3> points;

    Vector3 previousPoint;
	void Start () {
        previousPoint = transform.position;
        trailObjCount = 0;
        SetActive(true);
	}
	
	// Update is called once per frame
	void Update () {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SetActive(!active);
        }
        if (active == false)
        {
            return;
        }

        if (Vector3.Distance(previousPoint, transform.position) > segmentDistance)
        {
            NewPoint();
            previousPoint = transform.position;
        }
    }

    void NewPoint ()
    {
        points.Add(transform.position);

        // Rebuild mesh
        Vector3[] vertices = new Vector3[points.Count * 2];
        Vector2[] uvs = new Vector2[points.Count * 2];
        int[] tris = new int[(points.Count-1) * 6];

        for (int i=0; i < points.Count; i ++)
        {
            Vector2 delta = new Vector2(1, 0);
            if (i > 1)
            {
                delta.x = points[i].x - points[i-1].x;
                delta.y = points[i].y - points[i-1].y;
                delta.Normalize();
                delta = new Vector2(delta.y, -delta.x); // Get perpindicular vector
            }

            vertices[i * 2] = new Vector3(points[i].x - delta.x * lineWidth, points[i].y - delta.y * lineWidth, points[i].z);
            vertices[i * 2 + 1] = new Vector3(points[i].x + delta.x * lineWidth, points[i].y + delta.y * lineWidth, points[i].z);

            // UVs
            uvs[i * 2] = new Vector2(0 , 0);
            uvs[(i * 2) + 1] = new Vector2(0, 1);

            // tris
            if(i > 0)
            {
                // Triangles
                int triIndex = (i - 1) * 6;
                int vertIndex = i * 2;
                tris[triIndex+0] = vertIndex - 2;
                tris[triIndex+1] = vertIndex - 1;
                tris[triIndex+2] = vertIndex - 0;
               
                tris[triIndex+3] = vertIndex + 1;
                tris[triIndex+4] = vertIndex + 0;
                tris[triIndex+5] = vertIndex - 1;
            }
        }
        mesh.Clear();
        mesh.vertices = vertices;
        mesh.uv = uvs;
        mesh.triangles = tris;
    }

    public void SetActive (bool state)
    {
        if (state == true && active == false)
        {
            trailObjCount ++;
            active = true;

            points = new List<Vector3>(0);

            trailObj = new GameObject(gameObject.name+"_trail_"+trailObjCount.ToString());
            MeshFilter meshFilter = (MeshFilter) trailObj.AddComponent<MeshFilter>();
            mesh = meshFilter.mesh;
            trailObj.AddComponent<MeshRenderer>();
            instanceMaterial = new Material(material);
            trailObj.GetComponent<MeshRenderer>().material = instanceMaterial;
        }

        if (state == false)
        {
            active = false;
        }

    }
}

Comments

  • Just a question - is there a specific reason you need to rewrite a trail renderer? Does the standard (or optimised) renderer perform poorly in the same scenario? Just asking to understand your use case.
  • Aside from that, I would probably advise you to look at re-using the mesh and arrays as much as possible instead of re-creating them.

    But, if you are ok with the current performance of newpoint, then simply moving your variables out of the method into private class scope will already reduce gc at the small cost of keeping the used memory alive for longer.
  • edited
    The same applies for setactive. You are both creating a new game object as well as doing string concatenation - these don't play nice with gc. Try to re use objects and look at using string.format for the obj name.

    Sorry for the multiple posts.
  • Cool thanks for the reply man.

    So firstly : I need to rewrite the trailrenderer cause I want it to be distance based instead of time based. In the unity one you can only specify the lifetime of the segments.

    While researching I discovered that string concatenation is not kosher but this is not my fundamental problem so I am ignoring it for now. The main problem is that I need to recreate the vertices, uvs and tris arrays everytime I want to rebuild the mesh. Even if I were to move the array variables into the private class scope I am still recreating the array each time, which means the old array needs to be collected by the GC.

    I am aware that there is lots of room for improving the CPU performance of the script but it is kind of irrelevant until I find a solution to the garbage collector problem.
  • I tried writing a version where I used lists
    List<T> in C# uses arrays under the hood, allocating a new list is no more efficient than allocating a new array.
    So is this just a limitation of the unity Mesh implementation or can I do something about this ?
    This isn't really an issue with the Unity mesh implementation, it's just the way that graphics pipelines work.

    So, your GC problem is stemming from allocating new arrays every frame. Namely these three lines:

    Vector3[] vertices = new Vector3[points.Count * 2];
    Vector2[] uvs = new Vector2[points.Count * 2];
    int[] tris = new int[(points.Count-1) * 6];

    The "new" keyword is always a red flag when it comes to garbage generation. What you need to do is pool those vertices, uvs and triangles. In other words, just maintain one array for each that you keep around and re-use between frames.

    Updating the mesh (ie. mesh.vertices = blah) can still be costly, but can't really be avoided because the new mesh needs to be pushed to the GPU.

  • A quick and dirty solution comes to mind but I dont know how it will look in game. If you know the max size that a trail can be just pre build the arrays and only move the verts around. Extra ones can just bunch at the source (this is the part I dont know about) and the gpu will not even feel it.
  • @farsicon I actually think this will be the best in the end. But it feels "hacky"

    @Squidcor How would I go about reusing the array ? Are you talking of the same method as @farsicon ?

    Thanks for the replies so far.
  • @farsicon I actually think this will be the best in the end. But it feels "hacky".
    Indeed it does, and there could be more elegant approaches, but most optimisations eventually ends up hacky in some way :p
  • @Squidcor How would I go about reusing the array ? Are you talking of the same method as @farsicon ?
    Yup. Farsicon and I are basically talking about the same thing. You definitely want to declare your arrays at member level, rather than inside your update method. This will prevent them from being flagged for collection at the end of every update.

    However, now you have a different problem. Arrays are allocated with a fixed length up front, and you need the number of vertices in your mesh to change on a per frame basis.

    There are two ways around this (that I can see). Firstly, you can do what Farsicon is suggesting and just allocate some maximum number of vertices, and then just let all the extra ones sit on top of each other.

    A better method however would be to use Mesh.SetVertices(), and use lists instead of arrays. Note that lists can still cause allocations when you add to them (if the underlying array needs to "grow" to accommodate the new element). If this is a problem, you can set an appropriate capacity when you first create the lists. In terms of initial allocation, using a list with some large-enough capacity is the same as using an array (Farsicon's method), but only a slice of the underlying array is getting passed through to SetVertices, so you haven't got all the extra vertices in your mesh.

    I hope I explained this clearly enough?
  • Squidcor said:
    A better method however would be to use Mesh.SetVertices(), and use lists instead of arrays. Note that lists can still cause allocations when you add to them (if the underlying array needs to "grow" to accommodate the new element). If this is a problem, you can set an appropriate capacity when you first create the lists. In terms of initial allocation, using a list with some large-enough capacity is the same as using an array (Farsicon's method), but only a slice of the underlying array is getting passed through to SetVertices, so you haven't got all the extra vertices in your mesh.
    So while Mesh.SetVertices looks cool, isn't that just creating an array from the list of Vector3s anyway? I'd suggest profiling that to see if it's creating garbage, because I'd assume that it's basically doing a ToArray on the list and then shunting that through to the mesh - but that should definitely be tested.

    I don't think having extra vertices would be a trainsmash, I'd just put them at the end of the trail instead (assuming the trail fades out or scales to 0 width) because then you're not going to have a weird start to the trail. Might also be an idea to flip those extra verts to that clipping nukes them early.

    If the worry is about having loads of extra vertices if only a fraction of the total number of verts in the re-used array are being used, then it's a trivial matter to allocate two or three arrays of different sizes: few points, more points, ALL the points. Then you can switch between them and only use the smallest one that will hold all your current vertices.
  • So while Mesh.SetVertices looks cool, isn't that just creating an array from the list of Vector3s anyway?
    I haven't tested it, but I'm pretty sure SetVertices() doesn't do an array copy on the managed side. That's kinda why it exists.
  • Squidcor said:
    I haven't tested it, but I'm pretty sure SetVertices() doesn't do an array copy on the managed side. That's kinda why it exists.
    Yeah, I'd assume so too. But it's definitely worth checking. I mean, it has to build an array somewhere, but you're saying that it's not doing it in a way that's adding garbage, which means native code.
  • Ok cool this works like a charm.

    @Squidcor So the SetVertices way of doing things creates much less garbage. There is just a couple of infrequent garbage collections which I assume is due to the lists that needs to expand their underlying arrays. Many thanks for your help !!

    Here is the modified code :
    using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    
    public class TrailOptimized1_scr : MonoBehaviour {
        
        public Material material;
        public float segmentDistance;
        public float lineWidth;
    
        bool active = false;
        Material instanceMaterial;
        GameObject trailObj;
        Mesh mesh;
        int trailObjCount;
    
        List<Vector3> points;
        List<Vector3> vertices;
        List<Vector2> uvs;
        List<int> tris;
    
        Vector3 previousPoint;
    	void Start () {
            previousPoint = transform.position;
            trailObjCount = 0;
    	}
    	
    	// Update is called once per frame
    	void Update () {
            if (Vector3.Distance(previousPoint, transform.position) > segmentDistance)
            {
                NewPoint();
                previousPoint = transform.position;
            }
        }
    
        void NewPoint ()
        {
            points.Add(transform.position);
    
            Vector3 delta = new Vector3(1, 0, 0);
            if (points.Count > 1)
            {
                delta.x = points[points.Count-1].x - points[points.Count-2].x;
                delta.y = points[points.Count-1].y - points[points.Count-2].y;
                delta.Normalize();
                delta = new Vector3(delta.y, -delta.x, 0); // Get perpindicular vector
            }
    
            vertices.Add(new Vector3(points[points.Count-1].x, points[points.Count-1].y, points[points.Count-1].z)- delta * lineWidth) ;
            vertices.Add(new Vector3(points[points.Count-1].x, points[points.Count-1].y, points[points.Count-1].z)+ delta * lineWidth);
    
            // UVs
            uvs.Add(new Vector2(0, 0));
            uvs.Add(new Vector2(0, 1));
    
            // tris
            if(points.Count > 1)
            {
                // Triangles
                int vertIndex = (points.Count-1) * 2;
                tris.Add(vertIndex - 2);
                tris.Add(vertIndex - 1);
                tris.Add(vertIndex - 0);
               
                tris.Add(vertIndex + 1);
                tris.Add(vertIndex + 0);
                tris.Add(vertIndex - 1);
            }
    
            mesh.Clear();
            mesh.SetVertices(vertices);
            mesh.SetUVs(0, uvs);
            mesh.SetTriangles(tris, 0);
        }
    
        public void SetActive (bool state)
        {
            if (state == true && active == false)
            {
                trailObjCount ++;
                active = true;
    
                points = new List<Vector3>(0);
                vertices = new List<Vector3>(0);
                uvs = new List<Vector2>(0);
                tris = new List<int>(0);
    
                trailObj = new GameObject(gameObject.name+"_trail_"+trailObjCount.ToString());
                MeshFilter meshFilter = (MeshFilter) trailObj.AddComponent<MeshFilter>();
                mesh = meshFilter.mesh;
                trailObj.AddComponent<MeshRenderer>();
                instanceMaterial = new Material(material);
                trailObj.GetComponent<MeshRenderer>().material = instanceMaterial;
            }
    
            if (state == false)
            {
                active = false;
            }
        }
    }
  • edited
    Oooh, I didn't know about SetVertices(). Handy!

    As far as I know, when you create a new List, it's good to give it a number for a reasonable maximum that the list is likely to be (if your list is likely to change size regularly). I believe it reserves that space in memory, and internally has to create a new array if the list has to grow larger than that number. If you start with a zero-sized list, then I believe you're creating new arrays every time you add to the list. It could help to log how large your list typically ends up being, and just set it to be that large from the start. (Not a degreed programmer, but it's an speed/optimization problem I've had to deal with before on my own tools.)
    Thanked by 1Kobusvdwalt9
  • edited
    farsicon said:
    Just a question - is there a specific reason you need to rewrite a trail renderer? Does the standard (or optimised) renderer perform poorly in the same scenario? Just asking to understand your use case.
    There are some properties in TrailRenderer that cannot be set from code: EG: Colors.


    Some Performance Considerations:

    -- Lists are slow (slower than arrays, at least), but if your need them, use SetVertices on the mesh renderer. I use only arrays whenever I deal with procedural or dynamic meshes.

    -- Instead of using a MeshRenderer, you "could" just use Graphics.DrawMesh instead. This way you can render your trail with fewer overall components (you eliminate the need for both [i]MeshFilter and MeshRenderer)[/i]. You can put Graphics.DrawMesh() within an Update or LateUpdate() method. Also besure to use RecalculateBounds() on the mesh at least once before using Graphics.DrawMesh().


    Graphics.DrawMesh() also used to work when called within Coroutines as well, but since unity 5.3 this does not seem to function the same way anymore (things were simply not being rendered) - this is probably a bug.


Sign In or Register to comment.