perf: join the program lines as one sized copy #205

Closed
opened 2026-10-08 14:25:24 +00:00 by orion · 10 comments
Collaborator

Goal

The profile of the compiler's own build names Iterate.joined over the lines of the program (about 70 MB of C joined twice, pairwise in log n passes) at 4% of the build. Join the lines with one copy into a buffer of the right size, for program.c and per unit.

Design record

PERFORMANCE.md 9.6 ("The compiler's own build"). Part of #144.

Acceptance criteria

  • The emitted C is byte-identical before and after for the compiler (windows-x64, linux-x64), five conformance programs and tools/release-sync (sh tools/agree.sh emit hash lines equal to main's, compared in CI).
  • torb build --emit-c ./compiler is faster in the alternating-run comparison (expected about 4%; if the gain is below noise, say so in PERFORMANCE.md and the PR stays only if the code is not more complex).
  • sh tools/gates.sh a is green in forge CI.
  • Before/after numbers go into PERFORMANCE.md 9.7.

Measurement

Measured in CI, no local bootstrap: the forge CI job for the PR runs sh tools/gates.sh a; the before/after processor times come from torb check tests/conformance/adts.trb, torb build --emit-c tests/conformance/adts.trb and torb build --emit-c ./compiler run with the main binary and the PR binary alternating (3 runs, mean, as in PERFORMANCE.md 9.5), printed in the CI log and copied into PERFORMANCE.md 9.7. CI machines are noisy, so the ratio of alternating runs counts, not absolute seconds.

Seed impact

None if the sized copy uses natives and std names that already exist in the seed. If it needs a new native or std member the seed does not have, stop and split with the two-commit rule of CLAUDE.md (then it becomes an owner decision). One PR.

## Goal The profile of the compiler's own build names `Iterate.joined` over the lines of the program (about 70 MB of C joined twice, pairwise in log n passes) at 4% of the build. Join the lines with one copy into a buffer of the right size, for `program.c` and per unit. ## Design record [PERFORMANCE.md 9.6](https://git.torb.dev/torbscript/language/src/branch/main/docs/PERFORMANCE.md) ("The compiler's own build"). Part of #144. ## Acceptance criteria - [ ] The emitted C is byte-identical before and after for the compiler (`windows-x64`, `linux-x64`), five conformance programs and `tools/release-sync` (`sh tools/agree.sh emit` hash lines equal to `main`'s, compared in CI). - [ ] `torb build --emit-c ./compiler` is faster in the alternating-run comparison (expected about 4%; if the gain is below noise, say so in PERFORMANCE.md and the PR stays only if the code is not more complex). - [ ] `sh tools/gates.sh a` is green in forge CI. - [ ] Before/after numbers go into PERFORMANCE.md 9.7. ## Measurement Measured in CI, no local bootstrap: the forge CI job for the PR runs `sh tools/gates.sh a`; the before/after processor times come from `torb check tests/conformance/adts.trb`, `torb build --emit-c tests/conformance/adts.trb` and `torb build --emit-c ./compiler` run with the `main` binary and the PR binary alternating (3 runs, mean, as in PERFORMANCE.md 9.5), printed in the CI log and copied into PERFORMANCE.md 9.7. CI machines are noisy, so the ratio of alternating runs counts, not absolute seconds. ## Seed impact None if the sized copy uses natives and std names that already exist in the seed. If it needs a new native or std member the seed does not have, stop and split with the two-commit rule of `CLAUDE.md` (then it becomes an owner decision). One PR.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T21:17Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T21:17Z
Author
Collaborator

[loop:torbscript] release implement: the worker of the previous claim ended without a commit or a branch on the forge (its worktree is clean), so the claim is void; a new worker takes the issue below

[loop:torbscript] release implement: the worker of the previous claim ended without a commit or a branch on the forge (its worktree is clean), so the claim is void; a new worker takes the issue below
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T22:35Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T22:35Z
Author
Collaborator

[loop:torbscript] release implement: seed impact - the sized copy needs a new native, so this becomes a two-commit change and the loop has to decide it; no commit, no PR, the branch loop/205-sized-join was not pushed. usage: ~90k tokens / ~20 min. next: waiting (decision on the native, two-commit rule of CLAUDE.md)

What I found (on main 6374a3f0):

  • The two joins are emittedText (compiler/src/backend/c/emission.trb:152) and textOf (compiler/src/backend/c/units.trb:224), both lines.joined(separator: "\n") + "\n", which is concatenated in std/iteration/src/concatenate.trb: pairwise merges, log n levels.
  • No native the seed has can build a String of a size known only at run time in one copy:
    • Intrinsic.TextConcat (torb_text_concat / torb_text_concat_parts) takes a fixed number of parts, the number the interpolation was written with.
    • Nothing turns bytes into a String in one copy: String.copyBytes goes the other way, torb_text_try_from_bytes is only reachable through IO natives (File.readText, ...), and textOf in std/stream decodes character by character.
    • String.repeat, replace, split, sliceBytes and + cannot fill a buffer from a list either.
  • The callers need the String itself (File.writeText, units.header.hash(), torb run's cache key, the fixpoint), so writing the lines straight to the file would be a scope change.

Options:

  1. A native join (recommended, it is what the issue asks for): e.g. concatenated(pieces, separator) answered by the runtime (torb_text_join: measure, allocate once, copy once) in the C back end and in the VM. Under the two-commit rule: commit 1 teaches the native (with concatenated keeping its TorbScript body for the seed), seed refresh by the owner, then commit 2 uses it in emittedText and textOf. Since concatenated is a std function, the C of every program that joins text changes, so the "emitted C byte-identical" criterion would hold only if the compiler keeps its own join in compiler/src/backend/c and that is the only caller that changes.
  2. No native, wider merges: merge 8 or 16 pieces per level through one fixed-arity interpolation ("{a}{s}{b}{s}...", which is one TextConcat, one allocation). That makes log16 n levels (about 6 for ~1.7 M lines) instead of log2 n (about 21), so about 3-4x fewer bytes copied, but it is not one copy, and a 16-part interpolation is more code than the current loop. Expected gain: most of the 4%, unmeasured.

I did not start option 2 because the brief said to stop on seed impact rather than change the design of the issue.

[loop:torbscript] release implement: seed impact - the sized copy needs a new native, so this becomes a two-commit change and the loop has to decide it; no commit, no PR, the branch `loop/205-sized-join` was not pushed. usage: ~90k tokens / ~20 min. next: waiting (decision on the native, two-commit rule of CLAUDE.md) What I found (on `main` 6374a3f0): - The two joins are `emittedText` (`compiler/src/backend/c/emission.trb:152`) and `textOf` (`compiler/src/backend/c/units.trb:224`), both `lines.joined(separator: "\n") + "\n"`, which is `concatenated` in `std/iteration/src/concatenate.trb`: pairwise merges, `log n` levels. - No native the seed has can build a `String` of a size known only at run time in one copy: - `Intrinsic.TextConcat` (`torb_text_concat` / `torb_text_concat_parts`) takes a fixed number of parts, the number the interpolation was written with. - Nothing turns bytes into a `String` in one copy: `String.copyBytes` goes the other way, `torb_text_try_from_bytes` is only reachable through IO natives (`File.readText`, ...), and `textOf` in `std/stream` decodes character by character. - `String.repeat`, `replace`, `split`, `sliceBytes` and `+` cannot fill a buffer from a list either. - The callers need the `String` itself (`File.writeText`, `units.header.hash()`, `torb run`'s cache key, the fixpoint), so writing the lines straight to the file would be a scope change. Options: 1. **A native join** (recommended, it is what the issue asks for): e.g. `concatenated(pieces, separator)` answered by the runtime (`torb_text_join`: measure, allocate once, copy once) in the C back end and in the VM. Under the two-commit rule: commit 1 teaches the native (with `concatenated` keeping its TorbScript body for the seed), seed refresh by the owner, then commit 2 uses it in `emittedText` and `textOf`. Since `concatenated` is a std function, the C of every program that joins text changes, so the "emitted C byte-identical" criterion would hold only if the compiler keeps its own join in `compiler/src/backend/c` and that is the only caller that changes. 2. **No native, wider merges**: merge 8 or 16 pieces per level through one fixed-arity interpolation (`"{a}{s}{b}{s}..."`, which is one `TextConcat`, one allocation). That makes `log16 n` levels (about 6 for ~1.7 M lines) instead of `log2 n` (about 21), so about 3-4x fewer bytes copied, but it is not one copy, and a 16-part interpolation is more code than the current loop. Expected gain: most of the 4%, unmeasured. I did not start option 2 because the brief said to stop on seed impact rather than change the design of the issue.
Author
Collaborator

[loop:torbscript] decided by the loop: performance work and measurable, so measured first, not asked. Option 2 (no native) goes first: merge 16 pieces per level through one fixed-arity interpolation (one TextConcat, one allocation) in the compiler's own join in emittedText and textOf, so no seed step, one PR, emitted C byte-identical. Reasoning: it needs no seed refresh (that stays the owner's) and its gain is measured in forge CI against main (alternating runs, PERFORMANCE.md 9.7). If the measured gain stays well below the expected ~4%, a follow-up issue files option 1 (torb_text_join native under the two-commit rule) with the numbers. Next: implement.

[loop:torbscript] decided by the loop: performance work and measurable, so measured first, not asked. Option 2 (no native) goes first: merge 16 pieces per level through one fixed-arity interpolation (one `TextConcat`, one allocation) in the compiler's own join in `emittedText` and `textOf`, so no seed step, one PR, emitted C byte-identical. Reasoning: it needs no seed refresh (that stays the owner's) and its gain is measured in forge CI against `main` (alternating runs, PERFORMANCE.md 9.7). If the measured gain stays well below the expected ~4%, a follow-up issue files option 1 (`torb_text_join` native under the two-commit rule) with the numbers. Next: implement.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T22:40Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T22:40Z
Author
Collaborator

[loop:torbscript] implement: PR #213 (branch loop/205-sized-join, head 94c6b473) is open with option 2 as decided (linesText in the C back end, 16-way merges, no native, no seed step); the worker session ended before it read the forge CI result. CI on the PR is running (bootstrap linux-x64 green, gates and tier A/B pending). Red CI on the PR starts a fix round. Worker usage: not reported (session lost). next: waiting (forge CI on PR #213)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/213 (branch `loop/205-sized-join`, head 94c6b473) is open with option 2 as decided (`linesText` in the C back end, 16-way merges, no native, no seed step); the worker session ended before it read the forge CI result. CI on the PR is running (bootstrap linux-x64 green, gates and tier A/B pending). Red CI on the PR starts a fix round. Worker usage: not reported (session lost). next: waiting (forge CI on PR #213)
Author
Collaborator

[loop:torbscript] review: the merge gate asked for a boundary test of linesText and the numbers in PERFORMANCE.md; both are in head 41dacca2 on PR #213 (commit test(compiler): pin linesText at its edges, record the numbers in PERFORMANCE.md 9.8), review re-requested, CI running. next: waiting (merge gate review on PR #213)

[loop:torbscript] review: the merge gate asked for a boundary test of `linesText` and the numbers in PERFORMANCE.md; both are in head 41dacca2 on PR https://git.torb.dev/torbscript/language/pulls/213 (commit `test(compiler): pin linesText at its edges, record the numbers in PERFORMANCE.md 9.8`), review re-requested, CI running. next: waiting (merge gate review on PR #213)
Author
Collaborator

[loop:torbscript] claim review by session:loop-torbscript-20261008 until 2026-10-08T20:08Z

[loop:torbscript] claim review by session:loop-torbscript-20261008 until 2026-10-08T20:08Z
Author
Collaborator

[loop:torbscript] release review: PR #213 is red at 41dacca2 on one gate only, format --check (compiler/tests/lines-text.test.trb would be reformatted; every other gate was green). The fix worker could not be spawned (model account unavailable at the gateway). next: waiting (fix round: format the test file, retry when a worker model is available)

[loop:torbscript] release review: PR https://git.torb.dev/torbscript/language/pulls/213 is red at 41dacca2 on one gate only, `format --check` (`compiler/tests/lines-text.test.trb` would be reformatted; every other gate was green). The fix worker could not be spawned (model account unavailable at the gateway). next: waiting (fix round: format the test file, retry when a worker model is available)
argus closed this issue 2026-10-08 19:35:42 +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#205
No description provided.