Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

[FREE] More Effective Coroutines

Discussion in 'Assets and Asset Store' started by Trinary, Feb 23, 2016.

  1. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I'm really not sure how to answer this since "yield return Timing.WaitForOneFrame;" is not the kind of thing that can fail.
     
  2. WildStyle69

    WildStyle69

    Joined:
    Jul 20, 2016
    Posts:
    317
    Not sure what the problem was exactly, a Unity thing - all is working fine now. Thanks!

    // Wildstyle
     
  3. funkyCoty

    funkyCoty

    Joined:
    May 22, 2018
    Posts:
    678
    Hey there. I just purchased the PRO version of MEC. Sorry if this has been asked before (big thread) but it seems that in 2021.3+, Unity's Coroutines are actually much faster than MEC, at least for my (simple?) setup. I've got thousands of units which all run a single simple coroutine each (generally short lived, or yielding most frames). I swapped out unity's coroutines with MEC pro and profiled before and after. In a non deep profile, MEC is ~7ms for ~500 units. Unity eats up about ~3ms for the same ~500 units doing the same amount of work.

    MEC

    Unity_paXe9WFTUU.png

    Unity
    Unity_ETXe6nSJWT.png

    An example of how I swapped it out (seemed pretty straightforward?)
    Fork_fn82D6Mp9W.png

    Could there have been something I did wrong during setup to cause this? Or have Unity coroutines just gotten better since MEC was made?

    Edit: I was emailing the developer about this so I could be 100% sure I didn't do anything wrong. But, he confirmed that yes, MEC is just slower. He was also extremely rude, but that's aside the point. It's unfortunate, oh well.
     
    Last edited: Jan 18, 2023
  4. DwinTeimlon

    DwinTeimlon

    Joined:
    Feb 25, 2016
    Posts:
    294

    OMG, thanks so much for testing this out. I am using this for quite some time and was wondering if Unitys Coroutine improved. Oh well so I have to refactor a ton of code now and get rid of that thing.
     
  5. larryPlayablStudios

    larryPlayablStudios

    Joined:
    May 15, 2018
    Posts:
    28
    Gotcha. However, is there still an advantage to MEC because it doesn't allocate garbage, or has Unity improved on that front as well? I've been curious about going back to Unity coroutines so I can just use async and await instead of coroutines.
     
  6. Revolter

    Revolter

    Joined:
    Mar 15, 2014
    Posts:
    215
    Did anyone compare this to UniTask?
     
  7. rubenoriginal

    rubenoriginal

    Joined:
    Apr 23, 2018
    Posts:
    3
    I'm wondering that aswell... I'm currently working on a project and i'm not sure if i should swap to MEC or just use Unity coroutines.
     
  8. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,452
    I'm not exactly sure what the author of MEC actually means when he said that Unity allocates memory each frame. That was never the case. It was your own code that allocating garbage. When you do a
    yield return null;
    to wait one frame, no garbage is allocated. WaitForEndOfFrame and WaitForFixedUpdate can both be cached since they are just empty classes and the scheduler is only interested in the type of the instance to decide what to do with this coroutine. The only real problem was WaitForSeconds. Since the WaitForSeconds instance does nothing beside holding the wait time we can actually cache WaitForSeconds instances as well. Of course varying wait times was still an issue.

    However I created a solution for that by creating a single WaitForSeconds instance and with some CIL magic I'm able to change the wait time of that single instance so it can be reused everywhere. I haven't really looked at what "MEC" might do different. Though Unity's coroutines never really had that many issues. Once a coroutine is created and started, no new garbage is allocated except when you allocate memory
     
  9. NeloElber55

    NeloElber55

    Joined:
    Aug 3, 2021
    Posts:
    24
    Nice package, thanks
     
    Trinary likes this.
  10. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    Oh this is the guy that made some sort of tech demo where he ran one function (WaitUntilDone) over and over thousands of times and found that if he pushed it in non-logical ways (by running each corotine twice and making the two instances wait for each other) then he could find a way to make it run slightly slower than Unity's corotines.

    Maybe I'll take a look at WaitUntilDone and change it to run faster if you call it thousands of times per frame, but in the end MEC is made to be useful. It performs dramatically better than Unity's default coroutines in real applications. No, Unity hasn't made its coroutine implementation any faster in the last couple of years.
     
    WildStyle69 likes this.
  11. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    There's no reason why you can't use Unity's coroutines when you feel like it. MEC is still faster and has more features, but there's more documentation on Unity's coroutines.
     
  12. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    You would use MEC if you want to allocate less memory, run faster, or have more features in your coroutines. You can always try it out, there's a free version.
     
  13. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    yield return null used to create a memory alloc every frame, but Unity patched that somewhere around Unity 5. In terms of memory allocation it's awkward to have to deal with an object which holds the type of yield you want to perform. One of the ways that MEC is faster running than Unity's coroutines is that MEC uses a function call to wait for seconds rather than a reference object. A function call is faster and doesn't need to be cached, so your code is performant by default without having to write uninutitive caching.
     
  14. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I've done that... they are both pretty close in performance, and there are some ways in which UniTask is faster. In particular, UniTask is less centralized so it starts up faster. However, MEC coroutines outperform UniTask if you run a large number of them.

    The async await pattern can be less awkward for many people to use, especially for networking tasks. MEC Pro has a lot more animation and game logic features than UniTask. Personally, I've taken to using tasks for networking lately and coroutines for animations and game logic. Neither of the plugins are exclusive, so you can use them together.
     
    Revolter likes this.
  15. funkyCoty

    funkyCoty

    Joined:
    May 22, 2018
    Posts:
    678
    This is a lie. In my email, I detailed what I had done, so the author is intentionally lying.

    Here is some proof, some of the files from the 'swap out' commit in a full game project.

    upload_2023-7-15_9-21-9.png

    upload_2023-7-15_9-22-6.png

    upload_2023-7-15_9-22-22.png

    upload_2023-7-15_9-22-35.png

    upload_2023-7-15_9-22-47.png
     
  16. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,452
    I don't wnat to push this any further, but that was never the case. I use Unity since 2010 (version 2.6). What does allocate memory is when you use the wrong
    yield return 0;
    for example since 0 is an integer that needs to be boxed. This has always been wrong and I know some people still suggest this which is bad. A running coroutine does not actively allocate any memory besides the first Coroutine object when started and maybe some one-off internal stuff. Yes, using coroutines it's easy to accidentally allocate unnecessary memory, though when used "right" it does not allocate memory. This has never changed since the whole IEnumerator / yield mechanic is part of the C# compiler. I can understand that you want USPs to promote your solution. However claiming things which are not true doesn't help your reputation I guess.

    In regard to struct IEnumerable, the mono compiler had a bug in the past that made it box the struct because of the IDisposable at the end. Though as far as I know even that issue had been fixed in the mono compiler. Though that wasn't related to coroutines since coroutines only work with IEnumerators directly and interfaces always need to be "objects", otherwise you can not have a reference.
     
  17. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I literally made a video that's attached to the asset about the time that Unity changed their implementation of the coroutine concept so that yield return null wouldn't allocate memory. This asset is quite old as assets go, and I've been working with this exact problem for years. It seems strange to me to argue about history, but I was very active in this specific space when they changed it. I really wish they had changed it all the way to the ideal implementation. If they had I would have taken down my asset years ago since the whole reason I made MEC was for another asset I was working on that needed to not allocate memory every frame.

    TBH, arguing with people about the very nature of reality is not something I really want to be spending my time doing. I think a lot of people have stopped using coroutines completely lately in favor of the async await structure which is just easier to read than "yield return Timing.WaitForSeconds(2)". If I was building MEC today I would probably use that architecture as well and try to speed it up by taking away the multi-threading. I still use coroutines because they are faster, but I can see the case not to use them in today's world.
     
  18. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    The boxing issue was for foreach statements. Yes, that was fixed a few years ago.

    FYI, MEC uses IEnumerator<float> for coroutines, so it's not a reference type that is returned. There's really no reason to return a reference type in a coroutine since references are slower and use more memory than value types and you can perform all the same operations with value types. This is why you can do "yield return 0;" with MEC and it works just fine without allocating memory, but "yield return Timing.WaitForOneFrame;" is the most self documenting way to do it.
     
  19. WonkyDonky

    WonkyDonky

    Joined:
    Aug 24, 2014
    Posts:
    57
    I tried out the free version, on Unity 2022.3.2f1
    Code (CSharp):
    1. public IEnumerator<float> _MECCoroutineTest()
    2. {
    3.    yield return Timing.WaitForOneFrame;
    4. }
    I tried calling it every frame on Update:
    Code (CSharp):
    1. Timing.RunCoroutine(_MECCoroutineTest());
    it produces 24B of Garbage Collection every frame? Lower than 56B the normal coroutine did every frame, but still?

    Is there something wrong?
     
    Last edited: Sep 14, 2023
  20. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    That's right. It is not possible to completely eliminate GC allocs when creating a coroutine. In .NET a function pointer takes up 20 bytes and the minimum size of a variable in memory is 4 bytes. IEnumerators store the return value of the generator along side the function pointer. The moment you create a function (of any kind) that has a lifetime longer than "inline" .NET will have to copy the reference to that function out of stack memory and on to heap memory. This is what creates the unavoidable GC alloc.

    If you want to be more efficient with memory the best thing to do is to not create thousands of tiny coroutine functions every frame, but instead to reuse the ones you created last frame.
     
    WonkyDonky likes this.
  21. WonkyDonky

    WonkyDonky

    Joined:
    Aug 24, 2014
    Posts:
    57
    Alright, thank you

    Can you provide brief example code of how to correctly resuse coroutine in MEC? Or does it just mean having eg. while loop in the coroutine? Because I'm using coroutines as tasks, each task has its own custom code and it switches to a new task very frequently, soon as the last one ends, but what the next task is can't be predetermined
     
  22. Trinary

    Trinary

    Joined:
    Jul 26, 2013
    Posts:
    395
    I would just start one coroutine per type of task, and have the coroutine go through a list of uncompleted tasks and execute them by calling the function pointer to them. If you want to do a lot of optimization you can consider pooling the tasks (so they get added to an inactive list and the variables get set to the ones that a new task needs rather than creating new tasks).

    Real talk though: Running tasks by function pointer is never going to be amazingly fast. Every function you run by pointer requires a memory lookup. If you're looking for impressive results it's just going to be slow no matter what. What's quick is calling functions in a class directly. (and DOTS is even quicker.) Go ahead and use task queues because they are convenient and will get you from point a to point b without too much development, just don't spend too much effort trying to optimize them because you'll never get to an optimal state that way. An optimal state avoids events and function pointers.
     
    WonkyDonky likes this.