digitalmars.D - [Semi-OT] Fibers vs. Async / Await
- =?UTF-8?B?UmVuw6k=?= Zwanenburg (41/41) May 11 2022 The suggestion of adding async / await to the language comes up
- rikki cattermole (5/11) May 11 2022 Don't forget the extra cost of having to scan those stacks regardless of...
- =?UTF-8?Q?Ali_=c3=87ehreli?= (3/5) May 11 2022 While were on it, what are the blockers for stackless fibers for D?
- rikki cattermole (5/14) May 11 2022 I don't know.
- =?UTF-8?B?UmVuw6k=?= Zwanenburg (5/10) May 12 2022 That's a good point. As an improvement, could we mark the unused
- rikki cattermole (4/14) May 12 2022 I don't think it would help, and could potentially do the wrong thing.
- Steven Schveighoffer (4/14) May 12 2022 Fibers already do this.
- bauss (26/35) May 11 2022 You don't have to have a "Task" type that you declare, it could
- =?UTF-8?B?UmVuw6k=?= Zwanenburg (10/22) May 12 2022 Right. .Net doesn't do implicit conversion either, I was thinking
- bauss (21/43) May 13 2022 The await is necessary, just like yield is necessary for fibers.
- Sebastiaan Koppe (4/5) May 12 2022 Don't forget that fibers aren't supported on all platforms.
- IGotD- (16/19) May 12 2022 That was one of my observations when I looked in the druntime
- rikki cattermole (7/16) May 12 2022 The cost to maintain our fiber implementation is very minimal.
- =?UTF-8?B?UmVuw6k=?= Zwanenburg (3/4) May 12 2022 Didn't think of that. I only run D stuff on X86-64 at the moment
- rikki cattermole (3/8) May 12 2022 If you have phobos linked in, you are pretty much guaranteed to have fib...
The suggestion of adding async / await to the language comes up with some frequency, with fibers being seen as a lackluster alternative. Now, I feel like I must be missing something obvious, as fibers seem like the better option in almost every aspect to me. I'll compare them to async / await in .Net since that's the implementation I'm most familiar with. The issues I have with a/a: 1. When a function wants to do something asynchronous every function up the call stack needs to be in on it. This is especially problematic with generic code and higher-order functions. Even with D's powerful metaprogramming facilities I don't see a way to handle this nicely. As an example: the whole IEnumerable / IAsyncEnumerable situation in .Net is a manifestation of this problem. 2. It messes up your callstack. This leads to problems with: - Stack traces - Debugging - Memory dumps - Sampling profilers - And probably more that I haven't run into yet. Now, some tooling can kinda sorta reconstruct a usable stack, but all the efforts I've seen so far are still harder to use than when you have a proper stack. 3. I don't like the Task\<T> and await noise everywhere. It clutters up the code. 4. It's too easy to make a mistake and forget an await somewhere. Yes, an IDE will likely give a warning. No, I still don't like it ;) 5. It can put too much pressure on the GC. This won't be a problem when doing an HTTP request or something like that. It is a problem when there's an allocation for reading every single value in a DB query result. All the above problems don't exist when using fibers. The only downside of fibers I'm aware of is that you have a full stack allocated for every fiber. This can cost a lot of (virtual) memory, but on the other hand it makes for very effective pooling. And if you're in a situation where this really is a problem you can tweak the stack size. Seems like a small price to pay to me. Am I missing something? Thank you for your time.
May 11 2022
On 12/05/2022 1:27 AM, René Zwanenburg wrote:All the above problems don't exist when using fibers. The only downside of fibers I'm aware of is that you have a full stack allocated for every fiber. This can cost a lot of (virtual) memory, but on the other hand it makes for very effective pooling. And if you're in a situation where this really is a problem you can tweak the stack size. Seems like a small price to pay to me.Don't forget the extra cost of having to scan those stacks regardless of those fibers state. The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.
May 11 2022
On 5/11/22 06:49, rikki cattermole wrote:The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.While were on it, what are the blockers for stackless fibers for D? Ali
May 11 2022
On 12/05/2022 8:15 AM, Ali Çehreli wrote:On 5/11/22 06:49, rikki cattermole wrote: > The context state required for a task will be a lot smaller (and hence > cheaper) than what a single fiber stack will cost to scan. While were on it, what are the blockers for stackless fibers for D? AliI don't know. Whatever solution we come up with, it must support yielding in say a database driver, while an ORM returns say a Future and have the event loop automatically complete it.
May 11 2022
On Wednesday, 11 May 2022 at 13:49:28 UTC, rikki cattermole wrote:Don't forget the extra cost of having to scan those stacks regardless of those fibers state. The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.That's a good point. As an improvement, could we mark the unused part of the fiber's stack as NO_SCAN when it yields? And ofc undo it when the fiber switches back in. Or would that be too heavy an operation?
May 12 2022
On 12/05/2022 11:37 PM, René Zwanenburg wrote:On Wednesday, 11 May 2022 at 13:49:28 UTC, rikki cattermole wrote:I don't think it would help, and could potentially do the wrong thing. GC's like all memory allocators like to work with blocks of memory, that could easily overshoot the bounds of unused that were specified.Don't forget the extra cost of having to scan those stacks regardless of those fibers state. The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.That's a good point. As an improvement, could we mark the unused part of the fiber's stack as NO_SCAN when it yields? And ofc undo it when the fiber switches back in. Or would that be too heavy an operation?
May 12 2022
On 5/12/22 7:37 AM, René Zwanenburg wrote:On Wednesday, 11 May 2022 at 13:49:28 UTC, rikki cattermole wrote:Fibers already do this. https://github.com/dlang/druntime/blob/392c528924d0ce4db57a03bfdaa6587169568493/src/core/thread/fiber.d#L429-L436 -SteveDon't forget the extra cost of having to scan those stacks regardless of those fibers state. The context state required for a task will be a lot smaller (and hence cheaper) than what a single fiber stack will cost to scan.That's a good point. As an improvement, could we mark the unused part of the fiber's stack as NO_SCAN when it yields? And ofc undo it when the fiber switches back in. Or would that be too heavy an operation?
May 12 2022
On Wednesday, 11 May 2022 at 13:27:48 UTC, René Zwanenburg wrote:3. I don't like the Task\<T> and await noise everywhere. It clutters up the code.You don't have to have a "Task" type that you declare, it could simply be T and then the function is just marked async and T automatically becomes Task!T. envisioned the ability to add your own Task types etc. but that has never been implemented or anything, so it's really just verbose.4. It's too easy to make a mistake and forget an await somewhere. Yes, an IDE will likely give a warning. No, I still don't like it ;)It's easy to fix, don't allow implicit conversions between Task!T and T and thus whenever you attempt to use the result you're forced to await it because otherwise you'd get an error from the compiler due to mismatching types. Like the below would definitely fail: ``` async int getNumberAsync(); ... auto number = getNumberAsync(); int otherNumber = 10 * number; // number cannot be used here due to it not being implicitly convertible to int. The fix would be to await getNumberAsync(). ```5. It can put too much pressure on the GC. This won't be a problem when doing an HTTP request or something like that. It is a problem when there's an allocation for reading every single value in a DB query result.It doesn't really put anymore pressure on the GC. The compiler converts your code to a state machine and that hardly adds any overhead. It makes your functions somewhat linear, that's it. It doesn't require many more allocations, at least not more than fibers.
May 11 2022
On Thursday, 12 May 2022 at 06:04:06 UTC, bauss wrote:You don't have to have a "Task" type that you declare, it could simply be T and then the function is just marked async and T automatically becomes Task!T.That would help a little. Doesn't get rid of the await though.It's easy to fix, don't allow implicit conversions between Task!T and T and thus whenever you attempt to use the result you're forced to await it because otherwise you'd get an error from the compiler due to mismatching types.Right. .Net doesn't do implicit conversion either, I was thinking of functions that are void / just Task, like writing to a database. Assuming you use exceptions to report problems.It doesn't really put anymore pressure on the GC. The compiler converts your code to a state machine and that hardly adds any overhead. It makes your functions somewhat linear, that's it. It doesn't require many more allocations, at least not more than fibers.Are you sure about this? The state machine needs to be stored somewhere. I'd think we would need something similar to a delegate: a fixed-size structure that can be passed around, with a pointer to a variable sized context / state machine living on the heap.
May 12 2022
On Thursday, 12 May 2022 at 11:47:48 UTC, René Zwanenburg wrote:On Thursday, 12 May 2022 at 06:04:06 UTC, bauss wrote:The await is necessary, just like yield is necessary for fibers. They function much the same. The reason why you can leave out the await is because you might want to pass on the task to somewhere else, you might have a list of tasks that you have to wait for, so you need to be able to control when to wait for a task to finish and when not to wait.You don't have to have a "Task" type that you declare, it could simply be T and then the function is just marked async and T automatically becomes Task!T.That would help a little. Doesn't get rid of the await though.You can do it, but it's highly adviced against. There are only very few cases where you want to do it. For just Task there is no type and no result to await, so you just await the execution. I'm confused about why this is confusing to you and how you'd solve that?It's easy to fix, don't allow implicit conversions between Task!T and T and thus whenever you attempt to use the result you're forced to await it because otherwise you'd get an error from the compiler due to mismatching types.Right. .Net doesn't do implicit conversion either, I was thinking of functions that are void / just Task, like writing to a database. Assuming you use exceptions to report problems.The state machine can be optimized away pretty much and all you have is the current state which is like a couple bytes maybe and just tells what part of the machine is currently executing. This is not much different from storing information about what task is currently executing in a fiber. A statemachine is pretty much just a switch with a case for each state and then each step just changes to the next state.It doesn't really put anymore pressure on the GC. The compiler converts your code to a state machine and that hardly adds any overhead. It makes your functions somewhat linear, that's it. It doesn't require many more allocations, at least not more than fibers.Are you sure about this? The state machine needs to be stored somewhere. I'd think we would need something similar to a delegate: a fixed-size structure that can be passed around, with a pointer to a variable sized context / state machine living on the heap.
May 13 2022
On Wednesday, 11 May 2022 at 13:27:48 UTC, René Zwanenburg wrote:Am I missing something? Thank you for your time.Don't forget that fibers aren't supported on all platforms. They are definitely interesting but I don't think they should be the basis of everything.
May 12 2022
On Thursday, 12 May 2022 at 08:49:18 UTC, Sebastiaan Koppe wrote:Don't forget that fibers aren't supported on all platforms. They are definitely interesting but I don't think they should be the basis of everything.That was one of my observations when I looked in the druntime source code. The D runtime has basically implemented all of the scheduling itself, including all context switching. This means that it has to be implemented for all architectures, and variants of all architectures which is a lot. Many platforms like Windows has its own fiber API and I was surprised that druntime didn't use that one. I'm not saying that it is wrong to do the entire implementation in druntime but it is a lot of work and only the most common CPU architectures will be supported. There is also the risk that it becomes outdated as CPU vendors add stuff to their CPUs. I think since the D project has such limited resources, it should go for as generic solutions and reuse existing APIs. Async/Await is becoming accepted as a programming model and I think that D should put its effort to support that.
May 12 2022
On 12/05/2022 9:16 PM, IGotD- wrote:I'm not saying that it is wrong to do the entire implementation in druntime but it is a lot of work and only the most common CPU architectures will be supported. There is also the risk that it becomes outdated as CPU vendors add stuff to their CPUs. I think since the D project has such limited resources, it should go for as generic solutions and reuse existing APIs. Async/Await is becoming accepted as a programming model and I think that D should put its effort to support that.The cost to maintain our fiber implementation is very minimal. Prior to the refactor 3 years ago, a whole pile of the context switch assembly was last touched like 8-13 years ago. https://github.com/dlang/druntime/blame/3ead62a9bf4e0f866af10fdd3bc4edeb87237305/src/core/thread.d#L3754 ABI's are generally stable for this stuff. If they weren't a lot of userland would break and there would be no way to fix things.
May 12 2022
On Thursday, 12 May 2022 at 08:49:18 UTC, Sebastiaan Koppe wrote:Don't forget that fibers aren't supported on all platforms.Didn't think of that. I only run D stuff on X86-64 at the moment but cross-platform support is definitely important.
May 12 2022
On 12/05/2022 11:52 PM, René Zwanenburg wrote:On Thursday, 12 May 2022 at 08:49:18 UTC, Sebastiaan Koppe wrote:If you have phobos linked in, you are pretty much guaranteed to have fibers. You will either get a compile time error, or a link error if you didn't.Don't forget that fibers aren't supported on all platforms.Didn't think of that. I only run D stuff on X86-64 at the moment but cross-platform support is definitely important.
May 12 2022