My function appears to be creating garbage (GC Alloc) and i cannot see why

I wrapped a lerp via a static function and it appears to creating up to 160 bytes of garbage (seems slightly variable). Can anyone tell me why? Note SetTarget is always of the form (f) => myFloat = f

public static IEnumerator LerpFloat(float startValue, float endValue, Action<float> setTarget, float totalTime, Action onComplete = null)
    {
        float elapsedTime = 0f;
        while (elapsedTime <= totalTime)
        {
            float newValue = Mathf.Lerp(startValue, endValue, (elapsedTime / totalTime));
            setTarget(newValue);
            elapsedTime += Time.deltaTime;
            yield return Wait.WaitForFixedUpdate;
        }
        setTarget(endValue);

        if (onComplete != null)
        {
            onComplete();
        }
    }

It looks like you’ve cached WaitForFixedUpdate, so we can rule that out. The allocation could just be from starting the coroutine.

You could try rewriting it with UniTask or Awaitables, which use pooling to avoid garbage collection. If you want an async function to behave like a Coroutine, where it stops when the component is destroyed, then make sure to use the destroyCancellationToken.

// Ignores the component being destroyed
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);

// Returns if the component has been destroyed
if (await UniTask.Yield(PlayerLoopTiming.FixedUpdate, cancellationToken: this.destroyCancellationToken).SuppressCancellationThrow())
    return;

You can do the same in Update with less headaches of managing the coroutines. If you ever have multiple coroutines running and need to stop only specific ones they become a nuisance and a bugfest.

I’d wrap it up into a LerpValue struct that you call “LerpUpdate” on from Update().

I don’t and wouldn’t use coroutines for lerping. In fact, I mostly use them to delay something to the end of or next frame when the editor doesn’t allow me to Instantiate or Destroy an object from OnValidate or similar. There’s maybe a dozen StartCoroutine in 20k lines of code.

1 Like

I’d strongly disagree with this tbh. Coroutines are far more preferable than adding code in update() and require very little management.

1 Like

Does starting a coroutine create some garbage? That would be very interesting and not good :joy:

StartCoroutine returns a Coroutine object, which can be used to cancel a specific Coroutine.

It’s even more difficult to avoid GC if you want to use WaitForSeconds or WaitForSecondsRealtime without knowing the wait time in advance. There’s a way to avoid it for WaitForSeconds at least: When we need new instance of intrinsic YieldInstruction? - #4 by Bunny83

I’d recommend trying UniTask (or Awaitables if you’re using Unity 6) since they also make it easier to run tasks on a different thread. It’s difficult since you need to use thread-safe collections such as ConcurrentDictionary and check which internal Unity calls are thread-safe, but it can be a great tool for improving performance.

2 Likes

Yes, starting a coroutine does create garbage in two ways.

  1. When calling your generator method (a method returning an IEnumerator) you actually create your statemachine object that is returned by your generator method. How large this object is depends on the number of local variables you use in your coroutine. Local variables in your method become member fields of the generated class that represents your coroutine.
  2. When you pass this iterator object to StartCoroutine, Unity will internally create a Coroutine instance which it uses to manage the running coroutines.
  3. In your case specifically, when you pass (f) => myFloat = f as the callback action to your coroutine, it would also generate garbage as you create a closure here. This forces the compiler to move the location of “myFloat” into a shared instance so it can be referenced by the lambda function you created here.

So those things generate garbage. In case you didn’t know what a coroutine is, it is not a method. A coroutine is actually an object because when you use the yield keyword your “method” is turned into a statemachine object. When you call your method, all you do it creating an instance of that statemachine. If you want more details, I wrote a quite lengthy article about how coroutines work.

ps: the local variables of your coroutine are

float startValue; // 4
float endValue; // 4
Action<float> setTarget; // 8
float totalTime; // 4
Action onComplete; // 8
float elapsedTime; // 4
float newValue; // 4

So those alone are already 36 bytes which come on top of the usual object overhead (12 or 24 bytes depending on 32 or 64 bit). Though I wouldn’t care too much about how many local variables you may use in your coroutine. When you use coroutines, they are always classes and will generate garbage. If you want to avoid allocating garbage, your coroutine would need to continue to run and only started once. In some cases you can make the coroutine wait for a trigger boolean to start and loop back up so it’s essentially trapped in an inner loop waiting each frame to actually “start”. This avoids the garbage as the coroutine would always be running. It may sound silly, but it allows you to use all the neat coroutine features when it comes to complex sequences. For simple things like you did, it wouldn’t make much sense.

Technically it’s possible to “suspend” / stop a coroutine and resume (start it again) at a later point in time. However this is quite unconventional and requires a bit of book keeping. This would only get rid of the iterator instance memory as we would be reusing that, but when starting the coroutine again, Unity will again create a new Coroutine instance in order to register it to the scheduler.

There are alternative implementations of a custom coroutine scheduler which aim to avoid generating garbage, but you can’t really avoid it completely.

4 Likes

I used to think like that, actually I tried to force myself to believe it. But with enough of these issues I let go and never looked back. There are countless issues with coroutines:

  • garbage, eg by yielding objects
  • intransparent system:
    • the order of execution for coroutines may change, which may lead to really nasty issues if the initial order ABC changes to BCA because you stopped and restarted A. And then you do the same with C but also start D and then you have BADC and this keeps changing over and over.
    • don’t know which coroutine is currently scheduled or not without introducing your own Coroutine fields (which you need anyway if you want to stop the coroutine). There’s no IsRunning(TheRoutine) conditional.
    • There are unknown complexities behind the Start/StopCoroutine scheduling, and the yielding of objects.
    • hard to debug: you cannot follow your logic stepping through code as you can with a sequence of code in Update
  • Scheduling a coroutine multiple times (not stopping it, or assuming it would be stopped at this time) is a common source of bugs (hard to debug too).
  • blocks performance optimizations: to process multiple objects its best to run them through a single method that uses the components as opposed to each component performing its part by itself
  • potential for coroutines running on disabled components / objects (you surely know those console messages)
  • if you ever do something like this StartCoroutine(SomeOtherComponent.TheirRoutine()) you’ll better refactor - it’s “their” coroutine that runs on the current object :exploding_head:
  • Junior programmer (or bad hair day) disasters where you try to stop a coroutine like this: StopCoroutine(TheRoutine()) (same with a lambda) - the system will let you believe that this will work.

This is just a quick list of things from the top of my head (sure there’s more) why coroutines should be used sparingly and judiciously. IMO coroutines should never implement game logic but only the secondary aspects like animations or sounds or housekeeping tasks like yielding a webrequest.

1 Like

Worth remembering they’re a pretty ancient feature of the engine. One designed for convenience and not performance. And while it is a clever use of iterator objects, with the way they work, there’s no way around the allocations they create.

If you need performance or want to avoid allocations you need to look elsewhere. If you don’t need performance, then don’t care about it.

I’d be curious to see if using Awaitable’s is more performant allocation wise. UniTask would probably still win. But async code for this is probably overkill. But if this isn’t a fire-and-forget situation, run your logic in Update where you have the most control.

Btw, you can turn on Call Stack recording for GC.Alloc samples in the Profiler (the drop down in the middle of the window’s toolbar). That will get you callstacks all the way into mono/IL2CPP and therefore the necessary hints to figure out what exactly is causing them here.

You can see them in the Related data view or in Timeline, both when selecting GC.Alloc samples.

4 Likes