Search Unity

Coroutines memory allocation on every yield (bug?)

Discussion in 'Scripting' started by luispedrofonseca, Jun 4, 2015.

  1. luispedrofonseca

    luispedrofonseca

    Joined:
    Aug 29, 2012
    Posts:
    833
    Like the title says, I just realised that every yield on a coroutine generates a "small" memory allocation.
    While lots of resources around the web on the subject suggest that when returning null or when locally caching the yield instruction that can be avoided, that's not what I verify.

    Code (CSharp):
    1. while (true)
    2. {
    3.       yield return null;
    4. }



    Code (CSharp):
    1. while (true)
    2. {
    3.       yield return new WaitForFixedUpdate();
    4. }



    Code (CSharp):
    1. var waitForFixedUpdate = new WaitForFixedUpdate();
    2. while (true)
    3. {
    4.       yield return waitForFixedUpdate;
    5. }



    Is this to be expected and every resource out there that states that returning null or caching doesn't generate memory allocation is wrong or is there a bug introduced recently that makes this happen? I've tested with Unity 4 and 5 with exactly the same results.
     

    Attached Files:

    Last edited: Jun 4, 2015
  2. JamesLeeNZ

    JamesLeeNZ

    Joined:
    Nov 15, 2011
    Posts:
    5,615
    expected, however I thought you could cache your enumerator.

    I dont/wont ever use co-routines anyway.
     
  3. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,664
    A point to check, is it your yield instruction or the while loop producing the garbage?
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    6,991
    First off, your second one is definitely going to have more memory allocation, you're creating a new object... that takes up memory.

    As for when yielding null. This really doesn't have that much to do with unity, and more to do with .net/mono. Unity uses iterator functions from .net/mono for its coroutines, and this system is closely related to linq, which is notorious for generating little bits of garbage like this.

    They get cleaned up in time, it's fine and dandy. You shouldn't have to worry to much about them. Take your example... it's 17 bytes of memory. That's about the size of a Quaternion... (just on the heap instead). This really is only going to effect you if you've got a massive number of coroutines running at the same time.


    Things like this aren't "bugs". We're using a memory managed language, the heap is there FOR A REASON. It's what allows the framework to be memory managed. This automatic memory management comes at the cost of the garbage collector.

    The only bug I'd say that exists is the fact that Unity still uses such an outdated version of Mono. Mono has since optimized their garbage collector. But there's licensing disputes with Xamarin... so... ugh.
     
  5. luispedrofonseca

    luispedrofonseca

    Joined:
    Aug 29, 2012
    Posts:
    833
    The coroutine only has what you see on the code I posted.

    Yeah, me too... That's why I created this thread. It seems every other resource online about this matter is wrong.

    Totally understood what you're saying. The reason I called this a potential bug is because pretty much every resource online says you can avoid that memory allocation if you cache the enumerator or simply return null. That doesn't seem to be the case.


    While 17B is definitely not much, if you have a bunch of coroutines yielding on every frame it will eventually add up and trigger an "unnecessary" GC.
     
  6. luispedrofonseca

    luispedrofonseca

    Joined:
    Aug 29, 2012
    Posts:
    833
    Bump... Would really like to have a bit more perspective from someone on Unity's side.
     
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,625
    To update on what @lordofduct wrote, this is probably closely related to the issue where using foreach to iterate over a Collection (not an array) will create some garbage.

    In both instances, it is possible to do garbage-less iteration, as long as the generated IEnumerator is a struct. But, the version of Mono that Unity uses boxes that struct-IEnumerator in an object, and this creates garbage. I'm pretty sure that I've seen an Unity engineer mentuion that this is on the to-fix list, but as every other Mono-fix (as opposed to fixes on the Unity-side), it's probably not going to get worked on until the IL2CPP compilation is stable enough. Which is probably in a while.

    In the meantime, you'll have to live with those 17 bytes.

    EDIT: I pulled my info from here, which contains a short sum-up of the techinal side and some links.
     
    luispedrofonseca and DoomSamurai like this.
  8. luispedrofonseca

    luispedrofonseca

    Joined:
    Aug 29, 2012
    Posts:
    833
    Thanks for the great answer @Baste!

    Hopefully once the transition to IL2CPP is complete this issue will be addressed.
     
  9. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562
    This is only true in some cases. For instance, foreach on a dictionary produces garbage but not on a generic list. Also, there isn't a foreach in his hide, just a while and no collection but if you wanted to test you could try:

    Code (csharp):
    1.  
    2. for(var i = 0; i < 1)
    3. {
    4.   yield return null;
    5. }
     
  10. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,625
    I tested it. It generates garbage:

    Code (CSharp):
    1. void Start() {
    2.     StartCoroutine(Lol());
    3. }
    4.  
    5. IEnumerator Lol() {
    6.     for (var i = 0; i < 1; i++ ) {
    7.         yield return null;
    8.     }
    9. }
     
  11. Korno

    Korno

    Joined:
    Oct 26, 2014
    Posts:
    515
    May I ask why not? Being able to avoid having an Update/FixedUpdate function in a lot of my scripts - face it not everything needs to run everyframe , by using coroutines has saved me some big performance.
     
  12. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562

    Just once though or every frame? If it only generates 49 bytes of garbage when first invoked then its not a big deal. I intentionally left out the i++ by the way as I wanted it to keep running. You could say i < int.MaxValue.
     
  13. manlaikin1994

    manlaikin1994

    Joined:
    May 15, 2015
    Posts:
    179
    may i ask what is the tool to look for the memory?
     
  14. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,625
    Profiler. You find it under Window->Profiler.

    @Dustin: once per StartCoroutine. Which means that it might become a problem if you start a couple each frame and are on a mobile platform.

    So don't do that, I guess?
     
    manlaikin1994 likes this.
  15. Dustin-Horne

    Dustin-Horne

    Joined:
    Apr 4, 2013
    Posts:
    4,562
    yeah, I would expect it to generate garbage each time you start it (not necessarily while it's running though). It's because the return type is IEnumerator. It's returning an Enumerator. Internally it's creating one which is what the garbage is about.

    Here's an example that shows that too... let's say you have a dictionary that has 100 entries. When you do a foreach on the dictionary an enumerator is generated and put on the heap, but just once.

    Now let's say you have two dictionaries both with 100 entries. You do the following:

    Code (csharp):
    1.  
    2. foreach(var kvp in dictionary1)
    3. {
    4.     foreach(var kvp2 in dictionary2)
    5.     {
    6.            //do something
    7.      }
    8. }
    9.  
    This will generate 101 enumerators on the heap... one for the outer dictionary and one for the inner dictionary for every item in the outer. The StartCoroutine is doing something similar in that it's generating an Enumerator every time it's called.
     
unityunity