3
\$\begingroup\$

In Unity you can use:

    IEnumerator WaiterRoutine() 
    {
        yield return new WaitForSeconds(<seconds as float>);
    }

To wait a number of seconds. But this generates garbage for the garbage collector. I have a routine that waits for a random number of seconds every time it runs. So it just continually creates garbage.

My question is: is there any reason not to wait like this instead:

    public static IEnumerator WaitForSeconds(float seconds)
    {
        float start = Time.time;
        while (Time.time < start + seconds)
        {
            yield return null;
        }
    }

and if not, then why the heck is this not part of Unity's libraries? Is new WaitForSeconds somehow optimized further? Like does it remove my routine from consideration rather than busy waiting?

Just trying to figure out an efficient way to handle waiting.

Here's the actual code in question:

    void OnEnable() => routine = StartCoroutine(SpiritCannon());
    void OnDisable() => StopCoroutine(routine);

    private IEnumerator SpiritCannon()
    {
        while (true)
        {
            yield return Helpers.WaitForSeconds(Random.Range(minWaitSecs, maxWaitSecs));
            animancer.Play(spiritAnimation[animationCycler.Next()]);
        }
    }
\$\endgroup\$
7
  • 1
    \$\begingroup\$ Your new approach also generates garbage if it's chained to from within another coroutine. As do coroutines themselves, generally. That IEnumerator is syntactic sugar that tells the compiler "generate a new class for me that behaves like this function, and instantiate a new instance of it (ie create garbage) when I call that function". So while you might save an allocation by injecting these four lines into the middle of an existing coroutine (sharing its allocation instead of making a second one), you do not save an allocation by calling your new WaitForSeconds instead of the built-in one. \$\endgroup\$
    – DMGregory
    Commented Jun 12, 2021 at 10:04
  • 1
    \$\begingroup\$ The actual coroutine in question is an endless loop that waits random periods of time, so the only garbage should be the coroutine itself if I use the static method version of WaitForSeconds. Am I wrong? \$\endgroup\$ Commented Jun 14, 2021 at 18:26
  • \$\begingroup\$ Show us the code that calls this, and I'll show you where you're wrong. 😉 \$\endgroup\$
    – DMGregory
    Commented Jun 14, 2021 at 18:36
  • 1
    \$\begingroup\$ As DMGregory said if you use your own WaitForSeconds IEnumerator then instead of generating new YieldInstructions you will be generating new IEnumerators. To avoid that you must move the loop inside the "main" coroutine. BTW being static does not prevent it from creating new IEnumerator objects on each call just like say a static factory method. \$\endgroup\$
    – Nikaas
    Commented Jun 15, 2021 at 6:26
  • 2
    \$\begingroup\$ This line yield return Helpers.WaitForSeconds(Random.Range(minWaitSecs, maxWaitSecs)); allocates garbage. It instantiates a new instance of a class created from your static WaitForSeconds method, which includes the memory where it stores start and seconds. If you copy and paste the contents of WaitForSeconds into your SpiritCannon method instead of calling WaitForSeconds to start a new iterator, that would avoid that allocation. \$\endgroup\$
    – DMGregory
    Commented Jun 15, 2021 at 17:33

3 Answers 3

3
\$\begingroup\$

It sounds like the only way to use my code without consistently generating garbage is to code my example like this:

void OnEnable() => routine = StartCoroutine(SpiritCannon());
void OnDisable() => StopCoroutine(routine);

private IEnumerator SpiritCannon()
{
    while (true)
    {
        float seconds = Random.Range(minWaitSecs, maxWaitSecs);
        float start = Time.time;
        while (Time.time < start + seconds)
        {
            yield return null;
        }
        animancer.Play(spiritAnimation[animationCycler.Next()]);
    }
}

However, it should be possible to write a mutable custom yield instruction that doesn't generate garbage and can be modified/reused.

Here's unity's documentation.

And an example:

public class MutableWaitForSeconds : CustomYieldInstruction
{
    private float endTime;

    public MutableWaitForSeconds() : this(0f) { }
    public MutableWaitForSeconds(float seconds) => Reset(seconds);

    public void Reset(float seconds) => this.endTime = Time.time + seconds;

    public override bool keepWaiting => Time.time < endTime;
}

You would instantiate this custom yield instruction once, and then reuse it to avoid generating garbage.

\$\endgroup\$
2
\$\begingroup\$

Some time ago I actually found a solution for a garbage-free WaitForSeconds. Of course, as it was already mentioned, starting coroutines will always allocate memory. However, a certain fact about the WaitForSeconds class made it possible to simply reuse the same instance. The scheduler does not really store that WaitForSeconds instance internally. It just reads the wait time and forgets about the instance immediately. So all we have to do is change the internal float field of that instance.

This could be done with reflection, but since changing internal/private value type fields through reflection requires the value to be boxed, we're back to new garbage allocations. Though there's a solution. I made a small assembly over here that was simply written in IL and allowed me to manually call the constructor of the WaitForSeconds class again. This will not create a new instance, we just call the constructor which allows us to pass a new value and overwrite the current value.

So instead of:

yield return new WaitForSeconds(sec);

you can simply do:

yield return WaitForSecondsSingleton.Get(sec);

This will not allocate garbage as it uses a single WaitForSeconds instance. A bit hacky but works well.

Here's the compiled assembly and here's the IL source code.

\$\endgroup\$
0
\$\begingroup\$

With WaitForSeconds you directly queue the "resume" for its appropriate time. Haven't tested it but WaitForSeconds maybe is marginally faster (the longer the waiting period) because the checks/queuing is done "natively" by the engine and you don't do checks in addition to that it all the time. Using WaitForSeconds makes the code more straightforward to follow. When things get complex a lot of small readability issues quickly add up.

If your time is always the same you can avoid garbage by cashing WaitForSeconds as a YeldInstruction field and not generate a new one each time. BTW you can use the same cached one between multiple coroutines and that will not mess them up.

Your second example (while loop) advantage is that the time interval could change during wait. With WaitForSeconds once the wait begins you can not modify the wait time.

If you wait different amounts of time each time then the second example (with while loop) is probably better.

\$\endgroup\$
6
  • \$\begingroup\$ Thanks for your answer! Is this documented anywhere? I can't find anywhere that says WaitForSeconds is doing special scheduling at the "native" level. Is that surmise, or do you know that to be fact? \$\endgroup\$ Commented Jun 14, 2021 at 18:29
  • 1
    \$\begingroup\$ @Programmer9000 the observable ability to re-use a cached WaitForSeconds instance for multiple coroutines as Nikaas describes demonstrates that this must be the case, since the individual instance can't be storing any independent state about how long has been waited or what time to resume for each coroutine sharing the cached instance. \$\endgroup\$
    – DMGregory
    Commented Jun 14, 2021 at 18:39
  • 1
    \$\begingroup\$ @DMGregory I'm not sure how that proves that Unity has an optimized scheduling system for the WaitForSecond objects. It would be trivial to change my custom WaitForSeconds method to take a WaitForSeconds object while still re-using the same unoptimized busy wait solution. \$\endgroup\$ Commented Jun 15, 2021 at 17:33
  • \$\begingroup\$ If you can use the same YieldInstruction instance among parallel/multiple coroutines it means the "timer" is not there but inside the engine. \$\endgroup\$
    – Nikaas
    Commented Jun 15, 2021 at 17:47
  • 1
    \$\begingroup\$ Totally agree with you, that must be true. What I'm saying is that there's no evidence that using new WaitForSeconds, you gain the benefit of more efficient, non-busy-wait scheduling (like you'd find with operating systems putting processes to sleep). In point of fact, the documentation seems to suggest otherwise RE: custom yield instructions being checked every frame to keepWaiting. But it could be more efficient, I just can't find any evidence. \$\endgroup\$ Commented Jun 15, 2021 at 18:26

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .