offbynull / coroutines

Java toolkit that allows you to write coroutines.
GNU Lesser General Public License v3.0
346 stars 53 forks source link

Enhancement: Optimize 'pushing/popping' of method states #71

Closed offbynull closed 8 years ago

offbynull commented 8 years ago

Method states are popped off the stack as we restore and pushed back on as we suspend. There's no point in popping of the stack if we're going to push the same thing back on.

For example, imagine that we're suspended and we continue in to the following chain...

methodA(Continuation c) -> methodB(Continuation c) -> methodC(Continuation c)

methodA's state would get popped off the stack and loaded methodB's state would get popped off the stack and loaded methodC's state would get popped off the stack and loaded

methodC then decides to immediately suspend again.

methodC's state would get generated again and pushed on to the stack methodB's old state would get pushed back on to the stack methodA's old state would get pushed back on to the stack

The first 2 pops and the last 2 pushes can be avoided. Instead of doing this, have the continuation object keep an internal point to the current method state.

Instead of popping A and B off, just move the pointer. C would still get taken off and then pushed back on. Instead of pushing B and A back on, do nothing (just return).

The end result is the same, and we do less manipulation. So the final result would be...

methodA's state would get loaded and the pointer would move to methodB's state. methodB's state would get loaded and the pointer would move to methodC's state. methodC's state would get loaded and removed

methodC then decides to immediately suspend again.

methodC's state would get generated again and put back on the tail methodB would return without doing anything methodA would return without doing anything

offbynull commented 8 years ago

My idea fleshed out a bit more...

    // How does the cutpoint system work? Imagine that we started off restoring the following call chain...
    // runA() <-- savedMethodState[0]
    //  runB() <-- savedMethodState[1]
    //   runC() <-- savedMethodState[2]
    //    runD() <-- savedMethodState[3]
    //     runE() <-- savedMethodState[4]
    //
    // After the restore finishes, the following happens...
    // 1. runE() finishes running and returns
    // 2. then, runD() finishes running and returns
    // 3. then, runC() decided to call runX()
    // 4. then, runX() decided to call runY()
    // 5. then, runY() decides to call Continuation.suspend()
    //
    // So we left the bottom 2 existing method frames and entered in to 2 new method frames, and the callstack would now look like this...
    //
    // runA()
    //  runB()
    //   runC()
    //    runX()
    //     runY()
    //
    //
    // As we leave the restored continuation points in runE() and runD(), we set cutPointMethodState to previous method state. So after
    // runE()+runD() return, the cutPointMethodState would be at runC(). Everything after it is no longer valid...
    //
    // runA() <-- savedMethodState[0]
    //  runB() <-- savedMethodState[1]
    //   runC() <-- savedMethodState[2] / cutPointMethodState
    //    runD() <-- savedMethodState[3] (NO LONGER CONSIDERED VALID, BUT KEPT ANYWAS -- EXPLAINED FURTHER ON)
    //     runE() <-- savedMethodState[4] (NO LONGER CONSIDERED VALID, BUT KEPT ANYWAS -- EXPLAINED FURTHER ON)
    //
    //
    // As runX() and runY() suspend, they put their own method states as part of a new linked list: postCutPointMethodState...
    //   !!!WE ONLY START CREATING METHOD STATES AND ADDING THEM TO THE LIST AFTER THEY SUSPEND! THIS IS REALLY IMPORTANT TO REMEMBER!!!
    //
    // runX() <-- postCutPointMethodState[0]
    //  runY()  <-- postCutPointMethodState[1]
    //
    //
    // Then, once we successfully make our way up and out of the callstack, we merge these two lists together...
    // runA() <-- savedMethodState[0]
    //  runB() <-- savedMethodState[1]
    //   runC() <-- savedMethodState[2] / cutPointMethodState
    //    runX() <-- savedMethodState[3] / postCutPointMethodState[0]
    //     runY() <-- savedMethodState[4] / postCutPointMethodState[1]
    //
    //
    // Why do we use a separate list for new invocations (postCutPointMethodState)? Because if there's an uncaught exception, we
    // still want to keep the old one exactly the way it was. That's why we kept runD() and runE()s method state in the savedMethodState
    // even though we no longer considered them valid.
offbynull commented 8 years ago

Done. Be prepared to revert this change.