vm: retain and release of a one-pointer value as an instruction #164

Closed
opened 2026-10-08 00:36:49 +00:00 by orion · 12 comments
Collaborator

Goal

Make the retain and release of a value whose shape is one counted pointer an instruction of its own in the VM, instead of a call into the C kernel, the third open speed step of VM.md "Speed, the next steps".

Source

  • VM.md section 10, "Speed, the next steps": "a retain or a release is a C call of the kernel even for a value whose shape says it is one pointer, which could be one instruction of its own".
  • VM.md section 7, "Releases and destructors": identical to the C back end by construction (destructor queue, Close order, live-block counter).
  • runtime/machine.c torb_machine_release_block and torb_machine_retain_group: the semantics to keep (immortal count, shared count with atomics, a zero count is an internal error, a shape with a destructor queues the block with its last count in place).

Split out of #8.

A premise of #8 that does not hold for this step

fibonacci(30) takes and returns Int; it retains and releases nothing, so this step cannot show in that benchmark. It needs a workload that does retain: for example a record or closure passed and stored in a loop (benchmarks/closure.trb has the shape; a counted-record twin may be needed). Which workload is part of the decision below.

Acceptance criteria (once the mechanism is decided)

  • A retain and a release of a one-pointer shape (a record or enum block, a closure environment, Boxed) is one instruction of the loop with no kernel call on its common path (count above one, not shared, not immortal); the other cases fall back to the kernel exactly as today.
  • Nothing observable changes: destructor order, the live-block counter ("live blocks at exit: 0"), shared environments across workers and immortal constants behave as before; tools/conformance.sh --vm, leak gate, tier A and B green.
  • A chosen retaining workload measured before and after, and the numbers noted for VM.md section 8 (by #165).

Seed

Depends on the mechanism: a new Machine native (an inline retain/release in torb.h) is a seed-refresh change; reading and writing the count word with the existing Machine.load/Machine.store is not, but ties the interpreter to the runtime's block header layout.

Dependencies

Owner decision (comment below).

## Goal Make the retain and release of a value whose shape is one counted pointer an instruction of its own in the VM, instead of a call into the C kernel, the third open speed step of VM.md "Speed, the next steps". ## Source - [VM.md section 10, "Speed, the next steps"](https://git.torb.dev/torbscript/language/src/commit/d55c8d9c10edbaf9f48665bd782480736efbea03/docs/design/VM.md#L469): "a retain or a release is a C call of the kernel even for a value whose shape says it is one pointer, which could be one instruction of its own". - [VM.md section 7](https://git.torb.dev/torbscript/language/src/commit/d55c8d9c10edbaf9f48665bd782480736efbea03/docs/design/VM.md#L318), "Releases and destructors": identical to the C back end by construction (destructor queue, `Close` order, live-block counter). - `runtime/machine.c` `torb_machine_release_block` and `torb_machine_retain_group`: the semantics to keep (immortal count, shared count with atomics, a zero count is an internal error, a shape with a destructor queues the block with its last count in place). Split out of #8. ## A premise of #8 that does not hold for this step `fibonacci(30)` takes and returns `Int`; it retains and releases nothing, so this step cannot show in that benchmark. It needs a workload that does retain: for example a record or closure passed and stored in a loop (`benchmarks/closure.trb` has the shape; a counted-record twin may be needed). Which workload is part of the decision below. ## Acceptance criteria (once the mechanism is decided) - [ ] A retain and a release of a one-pointer shape (a record or enum block, a closure environment, `Boxed`) is one instruction of the loop with no kernel call on its common path (count above one, not shared, not immortal); the other cases fall back to the kernel exactly as today. - [ ] Nothing observable changes: destructor order, the live-block counter ("live blocks at exit: 0"), shared environments across workers and immortal constants behave as before; `tools/conformance.sh --vm`, `leak` gate, tier A and B green. - [ ] A chosen retaining workload measured before and after, and the numbers noted for VM.md section 8 (by #165). ## Seed Depends on the mechanism: a new `Machine` native (an inline retain/release in `torb.h`) is a seed-refresh change; reading and writing the count word with the existing `Machine.load`/`Machine.store` is not, but ties the interpreter to the runtime's block header layout. ## Dependencies Owner decision (comment below).
Author
Collaborator

[loop:torbscript] owner decision: how does the VM retain and release a one-pointer value without a kernel call?

What. A retain or release is Machine.operate into runtime/machine.c (torb_machine_retain_value, torb_machine_release_block) even when the shape is one counted pointer. To make it an instruction of the loop, something has to touch the block's count: an unshared block is count > 1 decrement or +1, but a shared block (several workers, atomic), an immortal one (0xFFFFFFFF), a count of zero (internal error) and a shape with a destructor (queue the block) must fall to the kernel as they do today.

Options.

  1. New inline natives (Machine.retainBlock(address) / a fast releaseBlock returning "not handled" for the slow cases), static inline in torb.h over the runtime's real header and atomics. Correct by construction, no layout duplicated in TorbScript. New native: two commits and a seed refresh, which the loop may not run.
  2. Existing Machine.load / store on the count word in interpret.trb: no seed, but the interpreter hard-codes the header layout (32-bit count in a 64-bit word, shared flag, immortal sentinel, little-endian), and any change to torb_header silently breaks it. Needs a test that pins the layout.
  3. Do not do this step: measure first. fibonacci(30) retains nothing, so the step is invisible there; a retaining workload (closure or counted-record loop) has to show a worthwhile win before a native or a duplicated layout is justified.

Recommendation. Option 3 first, then option 1 if the number justifies it: #165 (or this issue's first commit) adds the retaining workload to the measurement; if retain/release is under, say, 10 % of that run, drop the step. If it is large, option 1 over option 2 (layout stays in C). Say "go option 1/2/3".

[loop:torbscript] owner decision: how does the VM retain and release a one-pointer value without a kernel call? **What.** A retain or release is `Machine.operate` into `runtime/machine.c` (`torb_machine_retain_value`, `torb_machine_release_block`) even when the shape is one counted pointer. To make it an instruction of the loop, something has to touch the block's count: an unshared block is `count > 1` decrement or `+1`, but a shared block (several workers, atomic), an immortal one (`0xFFFFFFFF`), a count of zero (internal error) and a shape with a destructor (queue the block) must fall to the kernel as they do today. **Options.** 1. **New inline natives** (`Machine.retainBlock(address)` / a fast `releaseBlock` returning "not handled" for the slow cases), `static inline` in `torb.h` over the runtime's real header and atomics. Correct by construction, no layout duplicated in TorbScript. New native: two commits and a seed refresh, which the loop may not run. 2. **Existing `Machine.load` / `store` on the count word** in `interpret.trb`: no seed, but the interpreter hard-codes the header layout (32-bit count in a 64-bit word, shared flag, immortal sentinel, little-endian), and any change to `torb_header` silently breaks it. Needs a test that pins the layout. 3. **Do not do this step**: measure first. `fibonacci(30)` retains nothing, so the step is invisible there; a retaining workload (closure or counted-record loop) has to show a worthwhile win before a native or a duplicated layout is justified. **Recommendation.** Option 3 first, then option 1 if the number justifies it: #165 (or this issue's first commit) adds the retaining workload to the measurement; if retain/release is under, say, 10 % of that run, drop the step. If it is large, option 1 over option 2 (layout stays in C). Say "go option 1/2/3".
Author
Collaborator

Question q1 for torben (decision, blocking): How does the VM retain and release a one-pointer value without a kernel call?

v: 1
id: q1
to: torben
kind: decision
blocking: true
question: "How does the VM retain and release a one-pointer value without a kernel call? Details in the owner decision comment above."
options:
  - id: inline-natives
    label: New inline natives retainBlock/releaseBlock
    detail: Correct by construction over the real header; needs a seed refresh
  - id: load-store
    label: Machine.load/store on the count word
    detail: No seed, but the interpreter hard-codes the header layout; needs a layout test
  - id: measure-first
    label: Measure first
    detail: fibonacci(30) retains nothing; show a win on a retaining workload before choosing
recommendation: measure-first
asked_by: orion
**Question q1** for torben (decision, blocking): How does the VM retain and release a one-pointer value without a kernel call? ```hos-question v: 1 id: q1 to: torben kind: decision blocking: true question: "How does the VM retain and release a one-pointer value without a kernel call? Details in the owner decision comment above." options: - id: inline-natives label: New inline natives retainBlock/releaseBlock detail: Correct by construction over the real header; needs a seed refresh - id: load-store label: Machine.load/store on the count word detail: No seed, but the interpreter hard-codes the header layout; needs a layout test - id: measure-first label: Measure first detail: fibonacci(30) retains nothing; show a win on a retaining workload before choosing recommendation: measure-first asked_by: orion ```
Author
Collaborator

[loop:torbscript] refine: owner decision filed (q1), next: waiting (owner decision q1)

[loop:torbscript] refine: owner decision filed (q1), next: waiting (owner decision q1)
Author
Collaborator

[loop:torbscript] owner decision recorded (torben via WhatsApp, 2026-10-08 16:35 Europe/Berlin): "Okay, mess das. Frag so was nicht bevor du es nicht gemessen hast, so was kannst und sollst du proaktiv messen" (measure it; do not ask such things before measuring, measure proactively).

Resolution: measure-first. Scope: build a retaining workload benchmark, measure the kernel-call cost in CI (PERFORMANCE.md 9.5 method). If there is a clear win, pick the option on the merits yourself (inline-natives preferred, it is correct by construction; a seed refresh is fine); if not, record the numbers in PERFORMANCE.md and close as not taken. No further owner question needed. Labelled ready.

Standing guidance from the same answer: "Das meiste davon konntest du selbst beantworten" (most of this you could have answered yourself). Decide performance steps, seed refreshes that follow an existing plan, naming that follows the conventions, and anything measurable on your own; measure before asking; large refactors are fine when they make sense, size is no argument against an option.

[loop:torbscript] owner decision recorded (torben via WhatsApp, 2026-10-08 16:35 Europe/Berlin): "Okay, mess das. Frag so was nicht bevor du es nicht gemessen hast, so was kannst und sollst du proaktiv messen" (measure it; do not ask such things before measuring, measure proactively). Resolution: `measure-first`. Scope: build a retaining workload benchmark, measure the kernel-call cost in CI (PERFORMANCE.md 9.5 method). If there is a clear win, pick the option on the merits yourself (`inline-natives` preferred, it is correct by construction; a seed refresh is fine); if not, record the numbers in PERFORMANCE.md and close as not taken. No further owner question needed. Labelled `ready`. Standing guidance from the same answer: "Das meiste davon konntest du selbst beantworten" (most of this you could have answered yourself). Decide performance steps, seed refreshes that follow an existing plan, naming that follows the conventions, and anything measurable on your own; measure before asking; large refactors are fine when they make sense, size is no argument against an option.
Author
Collaborator

[question-closed] q=q1 reason=answered-elsewhere

[question-closed] q=q1 reason=answered-elsewhere
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T04:42Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T04:42Z
Author
Collaborator

[loop:torbscript] implement: PR #239, CI success, next: review (waiting on the merge gate argus)

Measured outcome (forge CI, PERFORMANCE.md 9.5 method: alternating binaries, processor time, mean of three, ratio):

  • A chain walk that retains and releases one pointer per step (benchmarks/chain-walk.trb, new) runs 1.15x faster with retain/release counted in the interpreter loop, in every pair (run 406: 1.156; run 419: 1.152 for the TorbScript form, 1.162 for the native form).
  • list-iterate improves by 1.06 to 1.14 and map-count by 1.02 to 1.09. Fibonacci, cons-stack, and a workload that keeps its values do not change beyond noise.
  • The TorbScript form (header masks via Machine.load/store) and the native form (Machine.countUp/countDown, static inline in torb.h) are equally fast. I chose the natives because they keep the header layout in C and keep the sandbox memory-stop site exact. That is the preferred inline-natives option.
  • All outputs match the base; part 2's conformance --vm passes 279/279. The numbers and reasoning are in PERFORMANCE.md 9.11.

Part 1 (PR #239, this lands now): the manifest entries, the static inline pair in torb.h, the regenerated torb_natives.h/machine_natives.c, five runtime tests, std/machine declarations, VM.md 6/10, the benchmark, and PERFORMANCE.md 9.11.

Part 2 needs a seed refresh first, one that contains Machine.countUp/countDown (two-commit rule; the refresh is the owner's). After it:

  • the emitter marks a one-pointer Kernel retain/release in the head: B = register + counted-word offset, C = 1 retain / 2 release. Old programs have C = 0, so the bytecode format stays torb-bytecode-1;
  • the interpreter's .Kernel arm calls countUp/countDown and skips the tail when they return true; otherwise it falls back to the kernel call.
  • A working reference is commit e6468e2d on branch loop/164-measure2 (measured in run 419 with part 1's binary as TORB_SEED).

Note: the first tier A run on the PR failed on two LSP diagnostics timing tests (language-server.test.trb, unrelated to this change). They passed on the rerun.

Token usage: ~0.39M tokens in this session since its last context summary (more before it).

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/239, CI success, next: review (waiting on the merge gate argus) **Measured outcome (forge CI, PERFORMANCE.md 9.5 method: alternating binaries, processor time, mean of three, ratio):** - A chain walk that retains and releases one pointer per step (`benchmarks/chain-walk.trb`, new) runs **1.15x faster** with retain/release counted in the interpreter loop, in every pair (run 406: 1.156; run 419: 1.152 for the TorbScript form, 1.162 for the native form). - list-iterate improves by 1.06 to 1.14 and map-count by 1.02 to 1.09. Fibonacci, cons-stack, and a workload that keeps its values do not change beyond noise. - The TorbScript form (header masks via `Machine.load`/`store`) and the native form (`Machine.countUp`/`countDown`, `static inline` in `torb.h`) are equally fast. **I chose the natives** because they keep the header layout in C and keep the sandbox memory-stop site exact. That is the preferred `inline-natives` option. - All outputs match the base; part 2's `conformance --vm` passes 279/279. The numbers and reasoning are in PERFORMANCE.md 9.11. **Part 1 (PR #239, this lands now):** the manifest entries, the `static inline` pair in `torb.h`, the regenerated `torb_natives.h`/`machine_natives.c`, five runtime tests, `std/machine` declarations, VM.md 6/10, the benchmark, and PERFORMANCE.md 9.11. **Part 2 needs a seed refresh first**, one that contains `Machine.countUp`/`countDown` (two-commit rule; the refresh is the owner's). After it: - the emitter marks a one-pointer Kernel retain/release in the head: B = register + counted-word offset, C = 1 retain / 2 release. Old programs have C = 0, so the bytecode format stays `torb-bytecode-1`; - the interpreter's `.Kernel` arm calls `countUp`/`countDown` and skips the tail when they return true; otherwise it falls back to the kernel call. - A working reference is commit e6468e2d on branch `loop/164-measure2` (measured in run 419 with part 1's binary as `TORB_SEED`). Note: the first tier A run on the PR failed on two LSP diagnostics timing tests (`language-server.test.trb`, unrelated to this change). They passed on the rerun. Token usage: ~0.39M tokens in this session since its last context summary (more before it).
Author
Collaborator

[loop:torbscript] implement: part 1 landed with PR #239. Part 2 (the interpreter arm using the counted-word offset, reference commit e6468e2d on branch loop/164-measure2) edits compiler/src/vm/interpret.trb like #163 part 2, which started now, and all worker slots are taken. next: waiting (#163 part 2 on the same file, no free worker slot) until 2026-10-09T10:45:00Z

[loop:torbscript] implement: part 1 landed with PR #239. Part 2 (the interpreter arm using the counted-word offset, reference commit e6468e2d on branch loop/164-measure2) edits `compiler/src/vm/interpret.trb` like #163 part 2, which started now, and all worker slots are taken. next: waiting (#163 part 2 on the same file, no free worker slot) until 2026-10-09T10:45:00Z
Author
Collaborator

[loop:torbscript] implement: #163 part 2 landed (PR #250), so the file is free, but part 2 of this issue cannot start yet: it needs a published seed that knows Machine.countUp/Machine.countDown from part 1 (d43017cd, landed 2026-10-09T03:14Z). The newest published seed, torb-seed-7ee364b7a06d, is from the commit before part 1. Refreshing the seed is not the loop's job; the next nightly seed after d43017cd unblocks it. next: waiting (published seed with the part 1 natives) until 2026-10-10T04:30:00Z

[loop:torbscript] implement: #163 part 2 landed (PR #250), so the file is free, but part 2 of this issue cannot start yet: it needs a published seed that knows `Machine.countUp`/`Machine.countDown` from part 1 (d43017cd, landed 2026-10-09T03:14Z). The newest published seed, torb-seed-7ee364b7a06d, is from the commit before part 1. Refreshing the seed is not the loop's job; the next nightly seed after d43017cd unblocks it. next: waiting (published seed with the part 1 natives) until 2026-10-10T04:30:00Z
Author
Collaborator

[loop:torbscript] implement: unchanged, part 2 needs a published seed with the part 1 natives. next: waiting (published seed with the part 1 natives) until 2026-10-10T04:30:00Z

[loop:torbscript] implement: unchanged, part 2 needs a published seed with the part 1 natives. next: waiting (published seed with the part 1 natives) until 2026-10-10T04:30:00Z
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T15:40Z (unblocked: the published seed torb-seed-5f22f071d02f of nightly-20261010 postdates the part 1 commit)

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T15:40Z (unblocked: the published seed torb-seed-5f22f071d02f of nightly-20261010 postdates the part 1 commit)
Author
Collaborator

[loop:torbscript] implement: PR #305 (head b4a6ba91, CI success, argus requested), est. tokens 90000, next: waiting (review by argus)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/305 (head b4a6ba91, CI success, argus requested), est. tokens 90000, next: waiting (review by argus)
argus closed this issue 2026-10-10 13:47:49 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#164
No description provided.