Spam with 100 coroutines that is just a timer - bad?

So, i read this, and got a conclusion, that coroutines shouldn’t be used even for simple timers too much, is that so, isn’t using the “if else” variable thing with time += Time.deltaTime even dumber?

Also i used a bunch of them with a script with burst (dont know does it better or not in this case) that does procedural generation, and it generated way faster than just leaving it a default method and i didn’t even had to do the async thing, lol.
(Though the usage was like 5-10 times in a row in Update(), so I didn’t see noticeable performance hit, comparing to leaving just one coroutine/void)

EDIT: 27.12.2024
By the way better not use async/sync Tasks for Timers, as they go even in Thread.Sleep + can be bad for time critical operations.
(P.S. Coroutines work the same as Update() timers, at least visually finish at the same time)

The best optimized robust way for timers I guess indeed is a Update() manager script where you assign all your timers which += Time.deltaTimer or -= (in 1 variable case) only inside a void().

Coroutines really only have a place for ‘fire-and-forget’ stuff in my mind these days. For stuff that needs to stop and start, or a certain degree of performance, or branching logic, there’s better options.

They always allocate, you always lose at least one a frame of execution (can be quite important), and they’re not really designed to be stopped from outside. Additionally, they’re tied to the lifetime of a game object, which limits situations where they can be applied.

I’ve found that async code be applied in more situations with more flexibility these days.

2 Likes

I pretty much mirror what spiney199 says and peronally have avoided them for as much as humanly possible for many many years now. Pretty much ever since I learned they allocated each iteration. It’s not as bad these days with the incremental GC but it’s just a habit of mine that I don’t ever see I’ll be dropping.

As a real example of how bad they can get: I was working on a mod for Daggerfall Unity a year or so ago and found that an inordinate number of objects in that game use coroutines. Pretty much every light source, NPC, and a ton of things in the player. It was showing up as one of the big allocators in the profiler and a noticeable source of time lost. Just for fun I ended up making my own branch of the entire game where I removed pretty much every single coroutine that ran during normal gameplay and replaced them with simple timer logic like what you demonstrated in an Update(). The result was easily a 10% increase in performance on average and probably closer to 20-30% in some infamously nasty scenes (like in Wayrest castle) or when a ton of combatant NPCs were present. It also removed a large amount of stutter due to the overworking of the GC (the game uses Unity 2019 so the incremental GC was off the table). So yeah, depending on the scenario the gains can be very real.

1 Like

I think this is what kids these days call a “big oof”. That’s crazy. I wonder how devs end up with these bad habits? I guess they’re not getting feedback on their code on where they can improve.

Or perhaps stubbornness. I’ve been on at least one modding community where it took a while to convince them that coroutines are not threading!

I wonder if a game on the scale of daggerfall (maybe not that big by today’s standard) would gain even more out of using a central Update manager to avoid too much marshalling between the C# and C++ sides.

Not to derail the topic too much but there was a lot that I saw in the code of DFU that was odd and hard to work with (player health is a completely different script from npc health, law enforcement is embedded directly into the core player logic, the list goes on and on). DFU was a passion project that (mostly) one individual worked on in their spare time for like a decade. Looking at the code I can clearly see a change in techniques and methodologies throughout so my guess is that they started with relatively little knowledge and learned a lot as they went. Kind of hard not to when you do something for THAT long! In the end they stuck with what they’d written because, wisely I’d add, they saw it was working and they were focused on the end goal of getting a finished thing out the door. The final game runs more-or-less flawlessly as is. It’s really only in a couple places in a slightly older build that anything was noticeably wrong (like Wayrest castle). Unfortunately, the modding scene is really a mess. Performance issues are quite common, both because modders are pushing the limits of what the game can handle and also because many of them really have no idea what they are doing. As for the centralizing update thing: For the base game I doubt it would make a huge difference but I had briefly considered it until I decided I didn’t care enough lol

To get back more onto topic: Coroutines are absolutely a tool that can solve problems. And for really small and simple stuff where you don’t care about performance or weird side-effects then a one-off event that happens at startup or on a level-load I think is fine. But when you start talking about numbers like ‘hundreds’ and you’re throwing them everywhere because they seem convenient and do what you want, that’s when things start to go south. That’s when things start to get into Javascript Programmer Land i.e. “If the code works it’s fine. Who cares how it works under the hood or what it’s doing?” Spending just a few moments considering the long-term ramifications and then a few more implementing something that is only very slightly more complicated can often save you a lot of headache.

1 Like

What’s perplexing to me is why Unity coroutines are so slow in comparison to Update(). I’m sure it checks the object’s existence every frame. So it’s that plus extra C#/C++ marshalling? The gap so huge I can’t think it’s just that. I created a coroutine runner to add some features and even with exception handling it’s way faster than a Unity Update() without much effort.

In my experience they were never slower and usually faster than Update(). But I haven’t measured that in years (maybe around the time of Unity 2017 was the last time I bothered to compare).

The real reason I avoided them was because of the garbage generation. Allocations aren’t free and that tended to scale worse as numbers got much higher. Eventually you are were going to kick off the dreaded hiccup of doom. The more you put pressure on the system to collect the longer and more frequent it was so it was well worth it to avoid absolutely any source of garbage generation that you could. Admittedly, this was was a much larger issue in the past before the incremental GC was implemented.

These days it’s more just a matter of habit. Even if the hiccup is much less noticeable than in the past, if it can still be avoided, why not? It’s low-hanging fruit that can easily contribute to improving the worst-case scenario, right? And now we have the tools to mostly avoid Coroutines anyway so all the more reason to do so. Anyone that has been doing this for a while likely has built systems to centralize Updates, built something on top of that to update with fix-frequency tick rates, and yet something else to easily manage one-shot timers. Between all of that and things like UniTask there really doesn’t seem to be much use for Coroutines anymore. Even Unity themselves seems to be leaning toward their eventual deprecation, so why not get ahead of the curve?

1 Like

I recently posted my concerns about coroutines here.

I mainly use them to delay code execution from OnValidate and similar messages where the editor doesn’t allow calling Destroy and some other methods without spamming you with warnings.

I do not even consider using them for game logic at all. It’s okay to have ONE coroutine method running in a MonoBehaviour but you’re going to have issues if you run more than one at a time.

For example, you will likely face issues if the weapon logic or player movement sometimes runs before, sometimes after the animation logic and the network synch - depending on when and where these individual coroutines get started and stopped, and whether they yield null, WaitForEndOfFrame, WaitForFixedUpdate, or WaitUntil.

Isn’t this what EditorApplication.delayCall is for?

Or a custom inspector.

Yes but this may cause a method on the script to run after delay regardless of whether that object got destroyed in the meantime, eg when exiting playmode. I suppose a similar thing may happen when entering playmode where you may not want that method to modify assets at runtime.

I’ve not had that issue with StartCoroutine and I’ve only given delayCall a try this week and quickly ran into such issues that I think it’s just not the right tool. Although I didn’t analyze the issue, I just added an Application.isPlaying check and moved on.

To be fair the issue there is using OnValidate for anything other than simple internal validation. It’s also another ancient part of Unity that’s probably better to not use anymore.

from c# to c++? Even when doing the il2cpp thing + burst, won’t in build both sides be c++ under the hood? Or the coroutines goes as an exception?

You should read this old article: 10000 Update() calls

Also that comment wasn’t with respect to coroutines, but lots of objects with Update calls.

Lets sum up all the comments.

Cons:

  1. Avoid to avoid headache in future. Especially big projects.

    (I guess if you have 10k lines of code total with frequent use, that’s already can be an issue, at least in performance
    (~10%? Though it was before Incremental GC))

  2. Don’t use it more than one in MonoBehavior

  3. It will be deprecated?

  4. That’s a newbie act, don’t do that on a job.

  5. Coroutines are not threading (Though as i remember the load on all cores still increased).

  6. Always allocate memory each iteration, even one, garbage will stuck.

  7. Tied to a lifetime of a GameObject with the script

Pros:

  1. It’s not as bad as was 'cause off Incremental GC, comparing to Unity 2019, which didn’t had it.

  2. (I guess, potentially, if not abusing, you still won’t end up leaking all the memory or performance and crashing some 2D platformer, though 3D already have load from graphics, so even I don’t think, that’s a good idea, though spamming with 10 of them in update was very convenient for instantiating a XZ grid made of planes/different prefabs, 'cause async is a bit harder to write)

Extra: Don’t spam with lots of Update() scripts also (That i did know already)

Special thanks to Sluggy1 for the most detailed answers.

2 Likes

In this case you’d still be marshalling because you’re moving from Engine Land to Script Land. The engine is more-or-less sandboxed off from your code regardless of how it gets compiled.

1 Like

True. I tend to overuse it for edit-time stuff because a separate editor script is just too much nuisance.

2 Likes

extra technical question if my update “return;” on first line after interval-timer finished it still ruins performance like an empty update or it gets ignored more?

Are you talking about Monobehaviour Update()? If so then yes, regardless of the contents you are paying the cost of having one.

Now whether or not that ‘ruins’ performance is entirely a different issue. It’s best to think in terms of scale. If you plan to have one of something then Update() in your scripts is fine. Hundreds? Probably still fine but it might be worth considering looking into a centralized system at some point. A thousand or so? You might see things creep into the profiler but it likely still won’t be a show stopper. Several thousands or more? Yeah, this is where it will become significant and needs to be dealt with another way.

1 Like

Very additional:
Now I got in a dilemma where i need to fix bug of unability to jump near walls when moving forward at the same time with single autojump (because of specific setup) in OnCollisionStay(), but if i do it through +=time.deltatime method even if i put it in Update not Stay it just dont work on 0.1f timer and below what i need (though 0.25f works perfect for some reason, even on the first jump).

So I guess in some cases coroutine is the only easy way, though i dunno if it will work better to false _isRestrictAutoJump thing. Maybe its just a bug of …Stay() that is calling more than once a frame even when restrict bool is true already or some frame skipping.

If interested i will reply if my amazing crutch will work

!! Edit: Nvm, today i fixed my 4 hour 3 AM mess in 25m, changed if (timer part) && !IsGround to && IsGround in ReturnSlopeCheck() timer part which += Time.deltaTime 's and remained only timer part else if
(Yep, no need to crutch with coroutine)

Asking just in case, is it okay of i disable script with update or is it still eating something but not polling? Or i should destroy it, if its not a mass destroy?