Repository navigation
Switch threads in resume by pointing at the coroutine's state - #52
Merged
Merged
Conversation
A resume copied the running thread's state out and the coroutine's in, and the yield copied them back, with the thread state's six pointer words taking a write barrier at every copy: five copies of 26 words per round trip, and the resume values and results copied once more each on the way. A generator ran 1.4 times slower than C Lua, and 5 times slower while the collector was marking. The executor now holds only the running thread's counters and frame list, which the loop reads without a hop, and points at the rest of its state, the stack and the side lists, in the thread itself. A switch copies the counters and the frame list, 13 words with two pointers, and sets the pointer; the thread waiting in resume keeps its state where tracebacks and the live walk read it, so the waiting copy goes. The running thread's copy is zeroed, so that no stale closure outlives the switch in a pooled executor, and the live walk counts the main thread's stack once, as the root, and another executor's main thread as a thread alone. The main thread's coroutine is made with the executor, holding the executor's first stack. resume appends what the coroutine passes back, its results or the values it yielded, to the caller's result buffer straight from the coroutine's stack, where yield values stay until the next resume, instead of copying them into a buffer of its own and out again; the value a wrapped coroutine failed with goes with that buffer too. The restore after the body is no longer deferred: a panic poisons the executor, which no one runs again. A host function that recovers a panic which unwound the VM through it ends its entry with the counters and the thread as the panic left them; such an entry now poisons the executor too, where it ran the next entry on a coroutine's stack. Values that the coroutine's stack cannot hold are refused before the switch, with the resumer's position, in the terms pushFrame asks for, where the resume that failed on the coroutine's thread named its yield site, or no position, and left it running forever, so that it could be neither resumed nor closed. The closers of a coroutine are placed by its own frames: the arguments of the resume or close that runs them lie on another thread's stack, and a call that returned leaves a record that places nothing, where placing closers above either grew a stack to the other's depth. A coroutine whose closer failed stays dead rather than running, one closed after dying in error leaves its stack to the next coroutine, as one that returned does, and one closed before it ran lets its body go. A generator of a million values goes from 83 ms to 54 ms, C Lua's 60; the suite's coroutine script from 8.3 ms to 5.4 ms, C Lua's 6.0; short coroutines that return values from 37 ms to 27 ms, C Lua's 33. Pointing the executor at the whole thread state made every Lua call take the hop for its frame and counters, 5 to 8 percent on fib and binarytrees; with those kept in the executor, the call-heavy scripts are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A resume copied the running thread's state out and the coroutine's in, and the yield copied
them back, with the thread state's six pointer words taking a write barrier at every copy:
five copies of 26 words per round trip, and the resume values and results copied once more
each on the way. A generator ran 1.4 times slower than C Lua, and 5 times slower while the
collector was marking.
Performance changes
the thread's counters and frame list, which the loop reads without a hop, and points at the
rest, the stack and the side lists, in the thread itself. A switch copies the counters and
the frame list, 13 words with two pointers, and sets the pointer. The thread waiting in
resume keeps its state where tracebacks and the live walk read it, so the
waitingcopygoes. The main thread's coroutine is made with the executor, holding its first stack.
back, its results or the values it yielded, to the caller's result buffer straight from the
coroutine's stack, where yield values stay until the next resume, instead of copying them
into a buffer of its own and out again.
one runs again, so nothing needs restoring on that path.
its stack to the next coroutine, as one that returned does; before, every failed wrapped
body allocated a fresh 16 KB stack.
Lua call take the hop for its frame and counters, 5 to 8 percent on fib and binarytrees.
Caching the pointer in the loop spilled across it. With the counters and frames kept in the
executor, the loop is unchanged and the gate's counts are identical.
Times on an Apple M-series machine against upstream main at 80f93c7, three rounds each, best
of five per run:
A note on fib: one build of this branch ran fib 10 percent slower while another, with the
same loop code and a small unrelated function added before it, ran it at upstream's time, as
did seven other such builds. The loop's machine code is identical, 2837 instructions either
way; the difference is code placement, which docs/benchmarks.md already notes moves single
rows by about 15 percent between builds.
Bug fixes
Most of these are pre-existing in the lines this change rebuilt; each has a test that fails
on the version before its fix.
failed on the coroutine's thread, named its yield site or no position at all, and left the
status at running, so the coroutine could be neither resumed nor closed. Values the
coroutine's stack cannot hold are now refused before the switch, with the resumer's
position, in the terms pushFrame asks for (
frameRoom), and the coroutine stays suspended.a coroutine's closers has its arguments on another thread's stack, and the record of a call
that had already returned could hold a position on yet another; placing a closer above
either grew the coroutine's stack to that depth, which failed a close from deep in the main
thread under an allocation limit. A thread records where its own Go calls begin
(
depthBase, asnnyBasedoes for yields), and a returned call's record is cleared, so itplaces nothing.
host function swallows a panic that unwound the VM through it, the entry completes with the
counters and the thread as the panic left them, and the pooled executor's next entry ran on
the coroutine's stack. Such an entry now poisons the executor, so it is never used again.
reach. The body goes with the close.
thread, and counted another executor's whole stack against an entry that held that
executor's main thread value. It counts the running thread's stack once, and another
executor's main thread as a thread alone.
result slice's length, where reset did not reach it. The record now holds it within length,
so reset lets it go.
Tests
New tests cover yield values read after the coroutine's stack grew, through nested resumes,
from inside pcall and after an error; a resume at the stack limit; a close from deep in the
main thread under an allocation limit; closers placed with a stale record; a panic recovered
by a host function and one recovered at a nested entry; stack reuse after an error; the main
thread's record after an entry; and the live walk's counting of the main thread, both this
executor's and another's. vet, tests, race, checkptr, stack stress, asmgate and the
generated-loop check pass locally; fuzzing was not run.
TestDifferentialagainst a C Luabuilt on macOS shows only pre-existing differences that main shows too.