diff --git a/CHANGELOG.md b/CHANGELOG.md index 660a39fdc..a364a2a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ The generated loop is now written from the program's source alone, and the manif - **A `main` that answers `Err` exits non-zero on wasm-gc and wasip2**, with the error on stderr on wasm-gc, as it already did on the VM and in generated Rust. Both wasm targets used to exit zero. - **`aver replay` of a run whose `main` answered `Err` matches.** The VM replay compared a runtime error against the recorded `Err` value and always reported a mismatch. - **The VM updates a Map in a field of a record in place when the record is consumed.** `State.update(state, counts = Map.set(state.counts, k, v), served = state.served + 1)` used to copy the whole Map on every call, even with `state` held by nothing else: the base of the update and the local, still needed for `served`, both held the record while the Map was set. A field that a record update or a record literal reads once, while every other read of the local in it is of another field and the local is not read afterwards, is now taken out of the record first, and the runtime does it only when exactly the holders the compiler accounted for hold the record. An update whose base nothing else holds moves the fields it keeps into the new record, so the dead base no longer keeps the next update of another field copying. In a release build, two thousand requests against a hundred-thousand-key Map now run in 0.06 s instead of 2.3 s, and ten thousand in 0.06 s instead of 9.6 s. A field two levels down (`setting.window.created`) is still copied; take the inner record out first. +- **The generated loop hands its own Maps on at the run's last use.** `__bump` (the answer versions), `__park` (the slot table), `__take` and the family seating read everything else of the run first and update the run last, so `Map.set` on `versions`, `slots` and a family's retired keys finds them held by nothing else instead of copying them once per answer on the VM. - **A generated loop updates an answer module's state in place on the VM too.** The loop hands the state out of the run in an `Option` and takes the answer back in a tuple, and on the VM the `Option` box and the tuple went on counting as holders of the state after the match had taken them apart, so every answer copied the module's Maps. A tuple, or the box of an `Option.Some`, `Result.Ok` or `Result.Err`, now counts its holders as maps, vectors and records do, and a `match` or `?` whose subject nothing reads afterwards releases what it holds when nothing else holds the tuple or box. The same goes for any program that carries a record through `Option`, `Result` or a tuple from one step to the next. `aver run --profile` also reports how many map entries the writes that were not in place copied. In a release build, 2000 requests against a 100,000-entry Map in the answer module's state run in 0.13 s instead of 1.8 s. - **The VM updates a Map in a field of a record in place when the record is consumed.** `State.update(state, counts = Map.set(state.counts, k, v), served = state.served + 1)` used to copy the whole Map on every call, even with `state` held by nothing else: the base of the update and the local, still needed for `served`, both held the record while the Map was set. A field that a record update or a record literal reads once, while every other read of the local in it is of another field and the local is not read afterwards, is now taken out of the record first, and the runtime does it only when exactly the holders the compiler accounted for hold the record. An update whose base nothing else holds moves the fields it keeps into the new record, so the dead base no longer keeps the next update of another field copying. In a release build, two thousand requests against a hundred-thousand-key Map now run in 0.06 s instead of 2.3 s, and ten thousand in 0.06 s instead of 9.6 s. - **The VM updates a Map further down a consumed record in place too.** `Setting.update(setting, window = Window.update(setting.window, created = Map.set(setting.window.created, k, v)), rounds = setting.rounds + 1)` and `Setting(window = absorbed(setting.window.created, setting.window.spent), rounds = setting.rounds + 1)` used to copy each Map on every call: `window` held it and `setting` held `window`. A path of fields read once in a record update or literal, under the same conditions as one field, is now taken out at the end of the path, and the runtime does it only when every record on the way down is held by exactly the holders the compiler accounted for: the record above it, and the bases of the updates the read sits in. A record on the way down is taken out of its parent when nothing else reads through it, so the update of that record moves the fields it keeps. A path read at the record's last use is taken the same way. `aver check` no longer reports such a path as `perf-shared-update`. In a release build, two thousand requests that each set a key in one hundred-thousand-entry Map and move a key to another, both inside a record inside the state, run in 0.04 s instead of 5 s. diff --git a/src/yield_lowering/coordinator.rs b/src/yield_lowering/coordinator.rs index 99d5a9d1a..cb9436b1d 100644 --- a/src/yield_lowering/coordinator.rs +++ b/src/yield_lowering/coordinator.rs @@ -594,7 +594,7 @@ fn write_loop( // ── Handing a state out ──────────────────────────────────────── for answer in answers { out.push_str(&format!( - "\nfn __take{0}(run: __Run) -> Tuple<__Run, Option<{1}>>\n ? \"Hands the state of '{2}' out of the run and leaves none behind, so the answer function it goes to holds the only reference to it and can update it in place. The state comes back with the answer.\"\n (__Run.update(run, {3} = Option.None), run.{3})\n", + "\nfn __take{0}(run: __Run) -> Tuple, __Run>\n ? \"Hands the state of '{2}' out of the run and leaves none behind, so the answer function it goes to holds the only reference to it and can update it in place. The state is read first and the run is updated at its last use, so the run it came from gives up its other fields instead of still holding them. The state comes back with the answer.\"\n (run.{3}, __Run.update(run, {3} = Option.None))\n", super::build::capitalize(&answer.field), answer.state, answer.module, @@ -670,12 +670,12 @@ fn write_loop( out.push_str("\nfn __current(run: __Run, id: Int) -> Int\n ? \"The instance number of the request one process is waiting on, or -1 when nothing is seated under that id.\"\n match Map.get(run.slots, id)\n Option.None -> 0 - 1\n Option.Some(slot) -> slot.seq\n"); out.push_str("\nfn __nextInstance(seq: Int) -> Int\n ? \"The instance number an answer for the current one leaves behind. It rises, so the instance just answered can never be current again.\"\n seq + 1\n"); out.push_str("\nfn __versionOf(run: __Run, owner: Int) -> Int\n ? \"How many times this answer module has answered other than Settled. A request parked on Settled is asked again once this has moved past the number it was parked at.\"\n match Map.get(run.versions, owner)\n Option.None -> 0\n Option.Some(version) -> version\n"); - out.push_str("\nfn __bump(run: __Run, owner: Int) -> __Run\n ? \"One answer of this module that was not Settled: its state may have moved, so every request parked on Settled with it may be worth asking again.\"\n __Run.update(run, versions = Map.set(run.versions, owner, __versionOf(run, owner) + 1))\n"); + out.push_str("\nfn __bump(run: __Run, owner: Int) -> __Run\n ? \"One answer of this module that was not Settled: its state may have moved, so every request parked on Settled with it may be worth asking again. The version is read first, so the versions Map is handed to Map.set at the run's last use.\"\n version = __versionOf(run, owner)\n __Run.update(run, versions = Map.set(run.versions, owner, version + 1))\n"); out.push_str("\nfn __deadlineOf(wake: Run.Wake) -> Option\n ? \"The deadline half of a wake, if it has one.\"\n match wake\n Run.Wake.Until(_, deadline) -> deadline\n Run.Wake.Settled(deadline) -> deadline\n"); out.push_str("\nfn __dueOf(deadline: Option, now: Int) -> Int\n ? \"The clock reading a deadline falls due at. A negative deadline is due now; no deadline carries none.\"\n match deadline\n Option.None -> 0\n Option.Some(ms) -> now + Int.max(ms, 0)\n"); out.push_str("\nfn __msOf(deadline: Option) -> Int\n ? \"How long the request asked to be left alone for, never less than nothing.\"\n match deadline\n Option.None -> 0\n Option.Some(ms) -> Int.max(ms, 0)\n"); out.push_str("\nfn __parked(slot: __Slot, wake: Run.Wake, now: Int, owner: Int, version: Int) -> __Slot\n ? \"The slot an Err leaves behind: the same instance and the same request, now remembering what would make asking again worth it. A deadline is turned into the clock reading it falls due at, and the ms that was asked for is kept beside it, so a clock that steps backwards cannot strand the request.\"\n __Slot(seq = slot.seq, pending = slot.pending, waiting = wake, due = __dueOf(__deadlineOf(wake), now), ms = __msOf(__deadlineOf(wake)), owner = owner, version = version)\n"); - out.push_str("\nfn __park(run: __Run, id: Int, wake: Run.Wake, owner: Int) -> __Run\n ? \"An Err: the request stays where it is with the same instance number. The state the answer module returned was already written back. An answer that is not Settled moves the module's version first; a Settled one does not, so a request cannot wake itself.\"\n moved = __moved(run, wake, owner)\n match Map.get(moved.slots, id)\n Option.None -> moved\n Option.Some(slot) -> __Run.update(moved, slots = Map.set(moved.slots, id, __parked(slot, wake, moved.now, owner, __versionOf(moved, owner))))\n"); + out.push_str("\nfn __park(run: __Run, id: Int, wake: Run.Wake, owner: Int) -> __Run\n ? \"An Err: the request stays where it is with the same instance number. The state the answer module returned was already written back. An answer that is not Settled moves the module's version first; a Settled one does not, so a request cannot wake itself. The clock and the version are read first, so the slots Map is handed to Map.set at the last use of the run.\"\n moved = __moved(run, wake, owner)\n now = moved.now\n version = __versionOf(moved, owner)\n match Map.get(moved.slots, id)\n Option.None -> moved\n Option.Some(slot) -> __Run.update(moved, slots = Map.set(moved.slots, id, __parked(slot, wake, now, owner, version)))\n"); out.push_str("\nfn __moved(run: __Run, wake: Run.Wake, owner: Int) -> __Run\n ? \"The versions after one Err: Until moves its module's version, Settled leaves it.\"\n match wake\n Run.Wake.Until(_, _) -> __bump(run, owner)\n Run.Wake.Settled(_) -> run\n"); out.push_str("\nfn __staleInstance(run: __Run, id: Int, seq: Int) -> Bool\n ? \"Why an answer carrying this instance number changes nothing: it is not the number the slot under this id is waiting on.\"\n seq != __current(run, id)\n"); @@ -858,7 +858,7 @@ fn write_seating(out: &mut String, proc: &Proc<'_>, performs: &ProcessEffects) { keyed.by, protocol.fn_name, keyed.field )); out.push_str(&format!( - "\nfn __seatFamily{upper}(run: __Run, keys: List<{key}>) -> __Run\n ? \"The '{0}' family at the turn boundary: instances whose key has left the list are dropped, retired keys that have left it may come back later, and every listed key that is neither seated nor retired is seated, in list order.\"\n{seat} present = __keySet{upper}(keys, {{}})\n kept = __dropLeft{upper}(run, Map.keys(run.seated{upper}), present)\n back = __Run.update(kept, retired{upper} = __unretire{upper}(kept.retired{upper}, Map.keys(kept.retired{upper}), present))\n __seatKeys{upper}(back, keys)\n", + "\nfn __seatFamily{upper}(run: __Run, keys: List<{key}>) -> __Run\n ? \"The '{0}' family at the turn boundary: instances whose key has left the list are dropped, retired keys that have left it may come back later, and every listed key that is neither seated nor retired is seated, in list order. The retired keys are listed first, so the retired Map is handed on at the last use of the run.\"\n{seat} present = __keySet{upper}(keys, {{}})\n kept = __dropLeft{upper}(run, Map.keys(run.seated{upper}), present)\n retiredKeys = Map.keys(kept.retired{upper})\n back = __Run.update(kept, retired{upper} = __unretire{upper}(kept.retired{upper}, retiredKeys, present))\n __seatKeys{upper}(back, keys)\n", protocol.fn_name )); out.push_str(&format!( @@ -957,7 +957,7 @@ fn write_serve(proc: &Proc<'_>, answers: &[Answer], performs: &ProcessEffects) - let mut pattern = binders.clone(); pattern.push("state".to_string()); out.push_str(&format!( - " {}.{}({}) -> match __take{}(run)\n (__rest, __held) -> match __held\n Option.Some(__taken) -> __serve{upper}{}(__rest, id, seq{key_arg}, state, {}.{op}({}))\n Option.None -> __rest\n", + " {}.{}({}) -> match __take{}(run)\n (__held, __rest) -> match __held\n Option.Some(__taken) -> __serve{upper}{}(__rest, id, seq{key_arg}, state, {}.{op}({}))\n Option.None -> __rest\n", protocol.request, kind.name, pattern.join(", "), diff --git a/tests/run_all_spec.rs b/tests/run_all_spec.rs index 7467372f5..afedb64c0 100644 --- a/tests/run_all_spec.rs +++ b/tests/run_all_spec.rs @@ -448,10 +448,15 @@ fn the_dump_shows_the_loop_that_was_generated() { "fn __servePeer(run: __Run, id: Int, seq: Int, request: __PeerRequest) -> __Run", // The answer module's state is handed out of the run for the one // answer, so the answer function holds the only reference to it. - "fn __takeLedger(run: __Run) -> Tuple<__Run, Option>", + "fn __takeLedger(run: __Run) -> Tuple, __Run>", "Ledger.claim(__taken)", // An Ok settles the request, an Err parks it on the wake it named. "Result.Err(__wake) -> __park(", + // The run's own Maps are handed to Map.set at the run's last use, + // with everything else read first, so no answer copies them. + " version = __versionOf(run, owner)\n __Run.update(run, versions = Map.set(run.versions, owner, version + 1))", + "__Run.update(moved, slots = Map.set(moved.slots, id, __parked(slot, wake, now, owner, version)))", + "(run.ledger, __Run.update(run, ledger = Option.None))", "Run.Wake.Settled(deadline) -> Bool.or(__versionOf(run, slot.owner) > slot.version", // The entry's own policies are called by name. "match admit(__view(run, ready), id)", diff --git a/tests/rust_work_spec.rs b/tests/rust_work_spec.rs index 3ccdb4801..6a1b95dff 100644 --- a/tests/rust_work_spec.rs +++ b/tests/rust_work_spec.rs @@ -446,7 +446,7 @@ fn an_answer_modules_state_reaches_its_answer_function_uniquely_owned() { "pub fn __serve(mut run @ _: __Run,", "pub fn __serveTicker(mut run @ _: __Run,", "pub fn __takeOwner(mut run @ _: __Run)", - "let (__rest, __held) = __takeOwner(run);", + "let (__held, __rest) = __takeOwner(run);", "crate::aver_generated::owner::bump(__taken, __a0)", ] { if !entry.contains(by_value) {