[llvm-dev] [RFC] LLVM Coroutines
Gor Nishanov via llvm-dev
llvm-dev at lists.llvm.org
Fri Jun 10 22:32:39 PDT 2016
Hi Eli:
>> Err... I guess nothing; sorry, I got myself confused.
That is my fault :-). I confused myself in the beginning where I was trying to
treat coro.suspend + cond.branch pair as a terminator and was looking for
branches. You showed me the way... Just ignore the branches and represent
the control flow cleanly and SimplifyCFG will take care of the rest :-).
>> You might run into problems with allocas:
>> x = alloca...
>>
>> block1:
>> suspend
>> ; increment could get moved here.
>> switch (resume, destroy, return)
>>
>> resume:
>> x += 1 ; load + inc + store
>> [...]
>>
>> destroy:
>> x += 1 ; load + inc + store
>> [...]
>>
>> return:
>> (don't touch x)
>> [...]
Yep. You are right. Need some kind of barrier to prevent the motion.
Okay. New intrinsics:
declare void coro.barrier(i64); // will get a unique number for every insertion
>> x = alloca...
>>
>> block1:
>> suspend
>> ; nothing can sneak in here???, Right?
>> switch (resume, destroy, return)
>>
>> resume:
>> coro.barrier(0)
>> x += 1 ; load + inc + store
>> [...]
>>
>> destroy:
>> coro.barrier(1)
>> x += 1 ; load + inc + store
>> [...]
>>
>> return:
>> coro.barrier(2)
>> (don't touch x)
>> [...]
Now we are good, right?
I am still not giving up hope for an intrinsic approach.
Thanks a lot, Eli, for your help with the design!
Gor
P.S
There is always a backup plan to go to your "two function" idea with a slight
twist.
Almost two functions (coro.fork(), coro.suspendO() same as in original proposal)
================================================================================
CoroEarly pass which runs at EP_EarlyAsPossible will extract f.start using
coro.fork as a guide (since false branch bypasses the coroutine body).
Coroutine will now travel as two functions until CoroSplit gets to f.start
(Probably will use 'coroutine' attribute on a function to indicate that it
requires CoroSplit to munge on it.)
I am still hoping we can find a solution without requiring to separate f.start
out. Seems like a huge hammer to avoid undesirable code motion / hoisting.
(I really hope that @coro.barrier(i64) approach can work)
On Fri, Jun 10, 2016 at 7:40 PM, Eli Friedman <eli.friedman at gmail.com> wrote:
> On Fri, Jun 10, 2016 at 5:25 PM, Gor Nishanov <gornishanov at gmail.com> wrote:
>>
>> Hi Eli:
>>
>> >> Naively, you would expect that it would be legal to hoist the store...
>> >> but that breaks your coroutine semantics because the global could be
>> >> mutated
>> >> between the first return and the resume.
>>
>> Hmmm... I don't see the problem. I think hoisting the store is perfectly
>> legal
>> transformation with all semantics preserved.
>>
>> Let's look at your example:
>>
>> >> block1:
>> >> suspend
>> >> switch (resume, destroy, return)
>> >>
>> >> resume:
>> >> store zero to global @g
>> >> doA()
>> >> [...]
>> >>
>> >> destroy:
>> >> store zero to global @g
>> >> doB()
>> >> [...]
>> >>
>> >> return:
>> >> store zero to global @g
>> >> doC
>> >> [...]
>>
>> As written the behavior is:
>>
>> 1) when we encounter a suspend during the first pass through the
>> function,
>> store 0 to @g and doC()
>>
>> 2) when we encounter a suspend after coroutine was resumed
>> ret void
>>
>> 3) When coroutine is resumed:
>> store 0 to @g and doA()
>>
>> 4) When coroutine is destroyed:
>> store 0 to @g and doB()
>>
>> If this is a coroutine that can be resumed asynchronously from a different
>> thread there is a data race. For example, if resume happens before 'f'
>> returns,
>> doA() can write something useful to @g, and 'f' will clobber it with zero.
>> But, this race is already present in the code and is not introduced by
>> LLVM.
>>
>> Let's hoist the store and see what happens:
>>
>> >> block1:
>> >> suspend
>> >> store zero to global @g
>> >> switch (resume, destroy, return)
>> >>
>> >> resume:
>> >> doA()
>> >> [...]
>> >>
>> >> destroy:
>> >> doB()
>> >> [...]
>> >>
>> >> return:
>> >> doC()
>> >> [...]
>>
>> Now, let's split it up:
>> 1. RAUW coro.suspend with -1,0,1 in f, f.resume and f.destroy
>> 2. remove coro.suspend in f, replace with ret void in f.resume
>>
>> void f() {
>> [...]
>> store zero to global @g
>> doC();
>> [...]
>> }
>>
>> void @f.resume() {
>> entry:
>> store zero to global @g
>> doA();
>> [....]
>> }
>>
>> void @f.destroy() {
>> entry:
>> store zero to global @g
>> doB();
>> [....]
>> }
>>
>> Behavior looks exactly as before. What am I missing?
>>
>
> Err... I guess nothing; sorry, I got myself confused. Essentially executing
> a return statement, then jumping back, seems wrong, but I'm having trouble
> coming up with a good example of how it would actually break something. I
> guess there aren't really any issues on a pure control-flow basis: you can't
> execute a global side-effect a different number of times than it would
> otherwise happen.
>
> You might run into problems with allocas: LLVM's optimizer will assume the
> lifetime of any alloca in the current function ends when you hit a "ret"
> instruction, so jumping back from the ret to the middle of the function
> could lead to wrong results. Silly example:
>
> x = alloca...
>
> block1:
> suspend
> ; increment could get moved here.
> switch (resume, destroy, return)
>
> resume:
> x += 1
> [...]
>
> destroy:
> x += 1
> [...]
>
> return:
> (don't touch x)
> [...]
>
> The increment is only supposed to execute once, but instead executes twice.
>
> This isn't a great example... and I'm not sure issues are limited to
> allocas... but that's the idea, anyway.
>
> -Eli
>
More information about the llvm-dev
mailing list