Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

C# Coroutine WaitForSeconds Garbage Collection tip

Discussion in 'Scripting' started by WarpB, Jan 27, 2014.

  1. ttesla

    ttesla

    Joined:
    Feb 23, 2015
    Posts:
    16
    Code (CSharp):
    1.  bool IEqualityComparer<float>.Equals (float x, float y) {
    2.                 return x == y;
    3.             }
    @Tochas I guess we shouldn't compare float values with "==" operator.
     
  2. phobos2077

    phobos2077

    Joined:
    Feb 10, 2018
    Posts:
    350
    My simple solution for case when I can't always use the same instance of WaitForSeconds because delay is random:

    Code (CSharp):
    1. public class WaitForSecondsMutable : CustomYieldInstruction
    2. {
    3.     private float waitUntil;
    4.  
    5.     public WaitForSecondsMutable()
    6.     {
    7.     }
    8.  
    9.     public WaitForSecondsMutable(float seconds)
    10.     {
    11.         waitUntil = Time.time + seconds;
    12.     }
    13.  
    14.     public WaitForSecondsMutable Wait(float seconds)
    15.     {
    16.         waitUntil = Time.time + seconds;
    17.         return this;
    18.     }
    19.  
    20.     public override bool keepWaiting => Time.time < waitUntil;
    21. }
    Usage is simple, just have one instance of this class per concurrent Coroutine and instead of:
    Code (CSharp):
    1. yield return new WaitForSeconds(delay);
    You write:
    Code (CSharp):
    1. yield return waiter.Wait(delay);
    Zero allocations per Wait call.
     
    khaled24 and Biggerandreas like this.
  3. yyylny

    yyylny

    Joined:
    Sep 19, 2015
    Posts:
    93
    That's a good idea but it won't work with concurrency. If you call waiter.Wait() for a second time before the first waiter.Wait() has returned it will restart the timer and both Wait() methods will return at the same time. For example:

    Code (CSharp):
    1.  IEnumerator Start()
    2.     {
    3.         StartCoroutine(Test());
    4.         yield return new WaitForSeconds(2);
    5.         StartCoroutine(Test());
    6.     }
    7.  
    8.     IEnumerator Test()
    9.     {
    10.         yield return waiter.Wait(5);
    11.         Debug.Log("Test");
    12.     }
    Instead of printing "Test" after 5 and 7 seconds it will print "Test" twice after 7 seconds. I could use another instance of WaitForSecondsMutable but it defeats the purpose of using less allocations.
     
    phobos2077 likes this.
  4. friuns3

    friuns3

    Joined:
    Oct 30, 2009
    Posts:
    307
    Code (CSharp):
    1. //some magic with Templates to avoid allocations
    2. //yield return Temp.WaitForSeconds(1);
    3. //yield return Temp.WaitForSecondsRealtime(1);
    4.  
    5. public partial class Temp
    6. {
    7.     public static WaitForSecondsRealtime WaitForSecondsRealtime(float interval)
    8.     {
    9.         return Cache(arg => new WaitForSecondsRealtime(arg), interval);
    10.     }
    11.     public static WaitForSeconds WaitForSeconds(float interval)
    12.     {
    13.         return Cache(arg => new WaitForSeconds(arg), interval);
    14.     }
    15.    
    16.     public static T Cache<T, T2>(Func<T2, T> func, T2 key)
    17.     {
    18.         if (Temp<Dictionary<T2, T>>.value.TryGetValue(key, out T t))
    19.             return t;
    20.         return Temp<Dictionary<T2, T>>.value[key] = func(key);
    21.     }
    22. }
    23.  
    24. public static class Temp<T> where T : new()
    25. {
    26.     public static readonly T value = new T();
    27. }  
    28.  
     
  5. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,888
    Isn't this potentially cueing yourself up for a memory leak if you use the Get method here in a dynamic way (rather than hardcoded time intervals)?
     
    phobos2077 likes this.
  6. Deuchie

    Deuchie

    Joined:
    Jan 28, 2021
    Posts:
    1
    No, at least not for me. I am new to Unity but not new to coding. Unity does coding in a special way, making me uncertain of almost everything I come up with. Now I am even searching for basic things just for sure.

    Everywhere I went I found that the coroutine examples used `yield return new` so I wondered if caching the object would be a bad idea, e.g. altering states inside (I've lost trust in almost anything since I found that I should not use != null on Unity Objects). That's why I came searching.
     
    Starburst999 likes this.
  7. Starburst999

    Starburst999

    Joined:
    May 8, 2017
    Posts:
    54
    You can pool them

    Code (CSharp):
    1. public override bool keepWaiting
    2. {
    3.     get
    4.     {
    5.         var waiting = Time.time < _waitUntil;
    6.         if (!waiting) Pool.Return(this);
    7.         return waiting;
    8.     }
    9. }
    And instead of instantiating you grab them from the pool. My code end up looking like this:

    Code (CSharp):
    1. yield return Wait.Seconds(0.25f);
    No concurrency issue, no alloc and no float dictionary.
     
  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,912
    Pooling actually isn't required when you use my (a bit hacky) workaround. It uses the fact that Unity's WaitForSeconds instance isn't really used or stored in the Coroutine. Instead the instance is just used to provide the wait time to the scheduler. So all we have to do is changing the internally stored wait time and reuse the single WaitForSeconds instance we have. This could be done though reflection, however this would again generate garbage since setting a value type through reflection requires boxing of the value.

    My solution is that I created a small assembly that essentially calls the constructor of the WaitForSeconds class again on the same instance. This will set the internal value without any boxing or garbage generation.

    Of course this solution is a little bit hacky as it relies on certain internal facts how WaitForSeconds works. Though this hasn't changed over the last 12 years so it's pretty safe to use.
     
    Starburst999 likes this.
  9. Starburst999

    Starburst999

    Joined:
    May 8, 2017
    Posts:
    54
    That's a really cool solution, glad you replied to this as I missed that thread when searching for a gc alloc free solution.

    Makes you wonder why this isn't like this out of the box in Unity...