[llvm-dev] Fwd: [RFC] LLVM Coroutines

Gor Nishanov via llvm-dev
Thu, 09 Jun 2016 20:17:39 -0000

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.