[llvm-dev] Fwd: [RFC] LLVM Coroutines
Gor Nishanov via llvm-dev
llvm-dev at lists.llvm.org
Thu Jun 9 13:17:39 PDT 2016
Hi Eli:
>> corobegin to label %coro.start
>> suspend label %retblock
>>
>> corosuspend [final] [save %token]
>> resume label %resume
>> cleanup label %cleanup
> Yes, that seems fine. There's still the potential for non-initialization
> instructions sneaking into the initialization block, but you can probably
> handle it somehow.
I don't mind non-initialization instructions sneaking in at all. I want them to.
All of the code that runs until it hits the suspends can stay in `f`.
Only post suspend code need to go to `f.resume` and `f.destroy`.
For example, consider fire and forget coroutine:
void f(int n) {
while (n-- > 0) {
do1();
<suspend> -- will subscribe to some async continuation
do2();
}
}
Post split, it will look something like this:
struct f.frame { Fn* ResumeFn, Fn* DestroyFn, int n };
void f(int n) {
f.frame* state = <init-stuff>;
state->n = n;
if (state->n-- > 0)
do1();
else
<destroy>
}
void f.resume(f.frame* state) {
do2();
if (state->n-- > 0)
do1();
else
<destroy-state>
}
void f.destroy(f.frame* state) { <destroy-state> }
> That said, thinking about it a bit more, I'm not really sure why you need to
> tie the suspend call to the branch in the first place.
I am not sure I understand the question. Suspend intrinsic is replaced with
a branch to a return block in the 'f' and with 'ret void' in resume.
> What exactly is your algorithm doing that requires it? I mean, a naive
> implementation of CoroSplit based on your llvm.experimental.coro.suspend
> intrinsic clones the whole function, replaces the result of suspend calls
> with "true" in one version and "false" in the other, and runs SimplifyCFG
> to kill the dead code.
Very close. We do clone function body twice, once for resume and once for
destroy. We make a new entry block with a switch instruction that will branch to
all resume branches in `f.resume` and to all cleanup branches in `f.destroy` and
then let SimplifyCFG to remove the rest.
Changing the subject a little bit. I was thinking we can get rid of the
coro.fork altogether it we add a third label to the corosuspend.
Namely:
corosuspend [final] [save %token] to label %return.block
resume label %resume
cleanup label %cleanup
corosuspend is lowered as follows:
in 'f': corosuspend is replaced with `br %return.block`
in 'f.resume':
add a new entry block with a switch jumping to all resume blocks
corosuspend is replaced with `ret void`
in 'f.destroy':
add a new entry block with a switch jumping to all cleanup blocks
corosuspend is replaced with `ret void`
I think this makes understanding of the model clearer. The only negative side
to a corosuspend with three branching targets is that it is very likely that
to label block will be the same in all corosuspend's thus wasting valuable
bits. :-)
Gor
P.S.
Return block in 'f' may contain a lot of stuff, like destructors for parameters
passed by value (if ABI requires), possibly some code related to return value.
`f.resume` does not have any of that, so the return block is just:
return.block:
ret void
P.P.S
I did consider making resume part return something other than void. One scheme
would be:
i1 false - at final suspend point (or destroyed)
i1 true - can be resumed again
If that case:
corosuspend => ret i1 true
corosuspend final => ret i1 false
control flow reaches the end of the `f.resume` ret i1 false.
I wasn't sure that it is a clear win, so I went with 'void' as a return value.
More information about the llvm-dev
mailing list