What is "GC.Collect" and why is it slowing down my game?

Before anything I’m well aware that Physics.CapsuleCastAll is another optimization issue, but at least that’s one I understand. What I DON’T understand is what this “GC.Collect” thing is, why it’s slowing down the game so much, and how I can go about fixing it.

To be fair, I have a FAIRLY good idea of what GC is. It’s the garbage collector that frees up the variable space occupied by temporary variables at the end of each update (roughly speaking). What I DON’T understand is why it’s eating so much ms especially after I tried to reallocate what variables I could outside of loops that would constantly use declarations as an excuse to eat up more space.

 Vector3 FindPath(float angle, float magnitude)
    {
        //Set up essential variables
        int direction = 0; //Left = 0, Right = 1
        Vector3[] directions = new Vector3[2];
        Vector3[] nodes = new Vector3[2];
        int loopLock = 0;
        //Assign nodes
        nodes[0] = transform.position;
        nodes[1] = transform.position;
        //Set up master loop variables
        float radius;
        RaycastHit[] hits;
        bool obstacleCheck;
        //Master loop. Only runs for a maximum of 1000 cycles
        do
        {
            //Left first
            {
                //Set direction
                direction = 0; // Left = 0
                //Set up other variables
                float leftAngle = AngleBetween(nodes[direction], destination);
                radius = cc.radius * transform.localScale.x + (cc.skinWidth + MARKER) * 2;
                hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(leftAngle), magnitude);
                obstacleCheck = true;
                //Comprehensive obstacle check
                if (!ObstacleCheck(hits, gameObject, leftAngle, magnitude))
                {
                    //Greater radius unit check
                    foreach (RaycastHit hit in hits)
                    {
                        if (obstacleCheck && hit.collider.GetComponent<TestScript>() != null && hit.collider.GetComponent<TestScript>() != this)
                        {
                            obstacleCheck = hit.collider.GetComponent<TestScript>().IsShovable(gameObject, leftAngle, magnitude);
                        }
                    }
                    //Lesser radius absolute check
                    if (obstacleCheck)
                    {
                        radius = cc.radius * transform.localScale.x + MARKER;
                        hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(leftAngle), magnitude);
                        obstacleCheck = ObstacleCheck(hits, gameObject, leftAngle, magnitude);
                    }
                }
                //Process obstacle check
                if (!obstacleCheck)
                {
                    //Create bisection variables
                    float offsetLow = 0;
                    float offsetHigh = 180;
                    //Bisect until the difference between offsetLow and offsetHigh is 1 or less
                    do
                    {
                        //Create working offset
                        float offset = (offsetHigh + offsetLow) / 2;
                        //Obstacle check at leftAngle - offset
                        radius = cc.radius * transform.localScale.x + (cc.skinWidth + MARKER) * 2;
                        hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(leftAngle - offset), magnitude);
                        obstacleCheck = true;
                        if (!ObstacleCheck(hits, gameObject, leftAngle - offset, magnitude))
                        {
                            //Greater radius unit check
                            foreach (RaycastHit hit in hits)
                            {
                                if (obstacleCheck && hit.collider.GetComponent<TestScript>() != null && hit.collider.GetComponent<TestScript>() != this)
                                {
                                    obstacleCheck = hit.collider.GetComponent<TestScript>().IsShovable(gameObject, leftAngle - offset, magnitude);
                                }
                            }
                            //Lesser radius absolute check
                            if (obstacleCheck)
                            {
                                radius = cc.radius * transform.localScale.x + MARKER;
                                hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(leftAngle - offset), magnitude);
                                obstacleCheck = ObstacleCheck(hits, gameObject, leftAngle - offset, magnitude);
                            }
                        }
                        //Reassign offsetLow or offsetHigh based on obstacle check
                        if (obstacleCheck)
                        {
                            offsetHigh = offset;
                        }
                        else
                        {
                            offsetLow = offset;
                        }
                    } while (offsetHigh - offsetLow > 1);
                    //Assign direction vector if it's empty
                    if (directions[direction] == Vector3.zero)
                    {
                        directions[direction] = AngleToVector(leftAngle - offsetHigh) * magnitude;
                    }
                    //Reassign node
                    nodes[direction] += AngleToVector(leftAngle - offsetHigh) * magnitude;
                }
                else
                {
                    //Return the vector at the left direction
                    return directions[direction];
                }
            }
            //Then right
            {
                //Set direction
                direction = 1; // Right = 1
                //Set up other variables
                float rightAngle = AngleBetween(nodes[direction], destination);
                radius = cc.radius * transform.localScale.x + (cc.skinWidth + MARKER) * 2;
                hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(rightAngle), magnitude);
                obstacleCheck = true;
                //Comprehensive obstacle check
                if (!ObstacleCheck(hits, gameObject, rightAngle, magnitude))
                {
                    //Greater radius unit check
                    foreach (RaycastHit hit in hits)
                    {
                        if (obstacleCheck && hit.collider.GetComponent<TestScript>() != null && hit.collider.GetComponent<TestScript>() != this)
                        {
                            obstacleCheck = hit.collider.GetComponent<TestScript>().IsShovable(gameObject, rightAngle, magnitude);
                        }
                    }
                    //Lesser radius absolute check
                    if (obstacleCheck)
                    {
                        radius = cc.radius * transform.localScale.x + MARKER;
                        hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(rightAngle), magnitude);
                        obstacleCheck = ObstacleCheck(hits, gameObject, rightAngle, magnitude);
                    }
                }
                //Process obstacle check
                if (!obstacleCheck)
                {
                    //Create bisection variables
                    float offsetLow = 0;
                    float offsetHigh = 180;
                    //Bisect until the difference between offsetLow and offsetHigh is 1 or less
                    do
                    {
                        //Create working offset
                        float offset = (offsetHigh + offsetLow) / 2;
                        //Obstacle check at leftAngle - offset
                        radius = cc.radius * transform.localScale.x + (cc.skinWidth + MARKER) * 2;
                        hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(rightAngle + offset), magnitude);
                        obstacleCheck = true;
                        if (!ObstacleCheck(hits, gameObject, rightAngle + offset, magnitude))
                        {
                            //Greater radius unit check
                            foreach (RaycastHit hit in hits)
                            {
                                if (obstacleCheck && hit.collider.GetComponent<TestScript>() != null && hit.collider.GetComponent<TestScript>() != this)
                                {
                                    obstacleCheck = hit.collider.GetComponent<TestScript>().IsShovable(gameObject, rightAngle + offset, magnitude);
                                }
                            }
                            //Lesser radius absolute check
                            if (obstacleCheck)
                            {
                                radius = cc.radius * transform.localScale.x + MARKER;
                                hits = Physics.CapsuleCastAll(nodes[direction], Top(nodes[direction]), radius, AngleToVector(rightAngle + offset), magnitude);
                                obstacleCheck = ObstacleCheck(hits, gameObject, rightAngle + offset, magnitude);
                            }
                        }
                        //Reassign offsetLow or offsetHigh based on obstacle check
                        if (obstacleCheck)
                        {
                            offsetHigh = offset;
                        }
                        else
                        {
                            offsetLow = offset;
                        }
                    } while (offsetHigh - offsetLow > 1);
                    //Assign direction vector if it's empty
                    if (directions[direction] == Vector3.zero)
                    {
                        directions[direction] = AngleToVector(rightAngle + offsetHigh) * magnitude;
                    }
                    //Reassign node
                    nodes[direction] += AngleToVector(rightAngle + offsetHigh) * magnitude;
                }
                else
                {
                    //Return the vector at the left direction
                    return directions[direction];
                }
            }
            //Increment
            loopLock++;
        } while (loopLock < 1000);
        //Report a theoretical infinite loop
        if (loopLock >= 1000)
        {
            print("Theoretical infinite loop detected");
        }
        //Return nothing if something goes wrong
        return Vector3.zero;
    }

So what are some things I could do to optimize this so that the garbage collector isn’t such a problem?

Your code creates a lot of garbage that GC must collect. More garbage - more time it will spend.
I can’t say what exactly in your code creates so much garbage, but you could enable allocation callastacks to track down all allocations.
here more info about allocation callastacks Unity - Manual: CPU Usage Profiler module

Well, I see tons of calls to ObstacleCheck(), CapsuleCastAll(), and GetComponent<>. I have no idea how much memory fragmentation they contribute, but the Caster calls surely aren’t free. I’m not sure that they contribute much, but if you can’t get around the CapsulaCast, try to minimize mem footprint by replaceing GetComponent by your own lookup table, and check your ObstacleCheck for hidden allocations.

0.8mb of memory allocated is quite a lot of memory to be allocated. The collector needs to look through all the memory and find if it has a reference to it or not. If you are allocating 0.8mb and it all needs to be cleared its going to take some time to do that.

A few pointers:

  • Avoid Foreach, I’ve had issues in the past using it and generating garbage. I believe that it has to do with boxing of structs so it becomes an object on the heap instead of sitting on the stack.
  • Physics.CapsuleCastAll is creating another array that will need to be collected. Use Physics.CapsuleCastNonAlloc and pass in a cached array that has been initialized to a reasonable size out side of the method of course.
  • Use GetComponent once and cache it in the loops, its an expensive call so if it needs to be used more than once cache it.
  • Avoid calling GC.Collect unless your loading a menu or something that would be less noticeable for any hiccups, but with 0.8mb allocated it will fire quite often no matter what.

To get rid of the GC problems you need to know were the allocations are coming from, set the profiler to Deep Profile. That will slow everything down but you get a more detailed out put.

How else am I suppose to filter out all of the colliders that lack that component? That’s what the purpose of that foreach is after all.

Use a standard for loop instead. The foreach implementation may create some memory fragmentation (or so I have read, but personally, I’ve never experienced it on a relevant scale myself).

So he was wrong and it does nothing significant then?

No, it means that I’ve never had to code so close to the limit with an foreach loop. The boxing issue rings a bell with me. But to be sure; how many CapsuleCasts are executed per resolution? Each one of them allocates an array, and the tip with pre-allocated arrays sounds very good to me.

At its peaks it’s around 1200. Why? Is that the source of all the garbage?

Well, that’s 1200 arrays allocated, and if you indeed are hit by the boxing issue (foreach run on a struct like RayCastHit) then definitely, yes, you are allocating some 2000-4000 items on the stack per call to findpath. Replacing CapsuleCast with *NonAlloc and the foreach loops with for (i) should reduce the fragmentation footprint significantly.

So I noticed something in the CapsuleCastNonAlloc arguments called “RaycastHit[ ] result”. Does this mean it returns an array of raycast hits like the traditional CapsuleCastAll if I simply put the array I want it to return to in that parameter?

Yes. You must initialize it before calling CapCast. The int returned is the number of objects in the buffer.

(and when I say ‘you must initialize it’ I mean something like

RaycastHit[] raycastHits = new RaycastHit[100];

which will create a single frame on your stack that will be re-used over and over)

you create garbage whenever you create temporary reference types

array is a reference type, thus your culprit is here

 RaycastHit[] hits;

every time you discard this array you will create as much garbage as you have filled this array with

how to fix:

declare a permanent array on the top of your script:

public RaycastHit[] hits;

and replace this part on your code

//RaycastHit[] hits;
Array.Clear(hits,0,hits.length);

By “permanent” you mean where all the global variables are, right?

i mean the variables that will appear on your inspector window if you make them public

You are creating a lot of new arrays every time that function is called. All Unity functions which return arrays create a new one when called.

Use the variants which take an array/list instead: those will write the results into the array/list you pass into them, so you can reuse the same array/list across multiple frames, by declaring them as private fields or static fields.

The same goes for temporary arrays you create inside functions. Declare those as fields outside the function and reuse them. The automatic memory management of C# is convenient, but comes with a very high performance cost when it comes to game development, meaning you have to code things in ways that are not obvious and not like what most C# examples teach.

I’m not entirely sure (not knowing how Unity’s implementation treats arrays of Structs), but: if arrays of struct are allocated as a single monolithic memory block (a buffer), allocate for a fixed size, and do not use array.clear; simply allow CapsuleCast to overwrite each existing item up to the number it returns. The single memory block can be deallocated instantly, or will never get collected if a global var (not an issue as long as you only have a reasonable buffer size).

So does the function clear the array automatically or do I have to do it myself before every CapsuleCastNonAlloc call?

Well that was fast. Preemptive even.