Weird that the yield statement is conflated with coroutines instead of iterators/generators. Even weirder to say its a "C# coroutine" feature when there's no coroutine in the C# language. It does look pretty similar to the yield keyword for C# iterators.
Unity (and probably others) use yielding iterators as a somewhat hacky way to make coroutines, though. I wouldn't say its a good role model but it gets the job done.
> Give me access to the generated struct. Allow me to put it on the stack of another function. Or as a member of another struct. Then I can store it on the heap if I want, but don’t force me.
As for why they're allocated on the heap...seems like the stack would be a foot gun, no? To write a safe coroutine, you need to hope your caller doesn't blow out the stack between iterations or just defensively put everything on the heap yourself anyway. Enforcing safety seems like the right call but that's a matter of taste, I suppose.
This does remind me of a recent C# change to the Task<T> promise type returned by async/await functions. Before they were only heap allocated because you need that to be able to properly await them multiple times. This is a major reason you can't use them in games and use things like Unity's Coroutines. As it turns out a lot of small heap allocs suck and stack allocation does come in handy for perf. Now we have stack allocated Task<T> types as well. It would be a nightmare if the unsafe was the default though... Why is life so complicated?
> Weird that the yield statement is conflated with coroutines instead of iterators/generators.
The concept of coroutines (Melvin E. Conway in 1963; who was also the source of FORK/JOIN) predates by a decade that of iterators/generators (which come from Alphard and CLU).
The term "yield" had already been used for many years for coroutines before being used for iterators/generators.
Usually the use of "yield" is not ambiguous, because the context is clear.
A C# iterator is a coroutine. A coroutine is a simply a function that can be suspended at a given point and resumed later. Iterators do exactly that at the point where you use "yield return".
My point is mostly that neither is called a coroutine in the language and if you Google C# coroutine you'll see mostly Unity results which are not official and may or may not be what the author was referring to.
C# 8 introduced async generators with IAsyncEnumerable, so you can combine yield and await in C# as well. Those async generators are consumed using the ”await foreach” statement (which offcourse can be used nested from within another async generator).
Not only there coroutines in C#, the machinery with structural typing that Microsoft proposed to C++ coroutines' design is largely based on how they work.
It certainly is, however it is hard to argue this point while looking at other more or less recent features:
string_view is basically a fancy "char *", with all the same use-after-free issues (same with other pointer range types and iterators)
Lambas with capture-by-reference can be freely copied and type erased into function<>, so obviously they did not care about safety when that feature got included
To really make string_view sane and safe, you'd have to make std::string reference counted (with each string view holding a ref), but I can't see that being popular.
Where you getting this information that tasks and IEnumerator can’t be used for games? I’ve been using Unity for 10 years and tasks and Ienumerators are used as coroutines all the time. Recently worked on Hello Kitty Island Adventure and almost all game logic goes through an async coroutine library. Most memory worries are in graphics and object overhead, not coroutine stack allocations.
I mean you don't want to use a Task for per frame work like and use a coroutine instead because tasks are pretty heap allocation heavy. When did I ever mention ienumerator?
> Weird that the yield statement is conflated with coroutines instead of iterators/generators.
I think the conflict is between "yielding another value" and "yielding control back to the scheduler". Both of them use "yield" but they're different types of yield, and people use the same keyword for both, and either one can be implemented in terms of the other, and so on.
They're not different though, right? Just two ways to think about what it means to return, no? How can you return a value without returning control to read that value? They're the same.
Still, I don't think iterators and coroutines are necessarily the same. You can imagine a coroutine that doesn't return a value and only happens to share a thread or something.
Regardless of the underlying mechanism that make them both work, programmers generally have a vastly different conception of yielding (that is, 'producing') a generated value as opposed to yielding (that is, 'ceding') execution time to the OS or a virtual machine.
That's the thing, since either one can be implemented in terms of the other, people conflate iterators/generators with coroutines as you've mentioned because they're easily interchangeable.
For anything that requires consistent low-latency you must be carefully to never do anything that allocates in the render loop. Even today, on powerful PCs, allocating just a few bytes risks degrading your performance massively especially when your users have contention (e.g. not just the game or media app running but also a stream, a music player, a web browser, random services...) as any call that could end up calling into the OS increases the chances for your thread to be context-switched
I often hear this, but in my experience of game design it’s simply not true — no modern game engine aims to do no allocations per frame. Lots of games are written in the scripting languages of various engines, which are all GCed.
The days of avoiding malloc in games is long since passed. Of course, fewer allocations, like reducing any work, is always better.
In a case like Unity, low latency stuff can be moved to the job/burst system. Coroutines are generally for gameplay logic. Things that need to be done overtime in a single context.
The yield keyword was also used in green-thread coroutine / cooperative multitasking systems simply meaning 'yield control back to the task scheduler'.
> Even weirder to say its a "C# coroutine" feature when there's no coroutine in the C# language.
Unity has a coroutine system based on iterators, maybe that's where the confusion is coming from:
Unity (and probably others) use yielding iterators as a somewhat hacky way to make coroutines, though. I wouldn't say its a good role model but it gets the job done.
> Give me access to the generated struct. Allow me to put it on the stack of another function. Or as a member of another struct. Then I can store it on the heap if I want, but don’t force me.
As for why they're allocated on the heap...seems like the stack would be a foot gun, no? To write a safe coroutine, you need to hope your caller doesn't blow out the stack between iterations or just defensively put everything on the heap yourself anyway. Enforcing safety seems like the right call but that's a matter of taste, I suppose.
This does remind me of a recent C# change to the Task<T> promise type returned by async/await functions. Before they were only heap allocated because you need that to be able to properly await them multiple times. This is a major reason you can't use them in games and use things like Unity's Coroutines. As it turns out a lot of small heap allocs suck and stack allocation does come in handy for perf. Now we have stack allocated Task<T> types as well. It would be a nightmare if the unsafe was the default though... Why is life so complicated?