Skip to content

perf(check): a call's pending arguments are a cons list, and parameters flatten in one loop - #1967

Merged
nicolas-abril merged 2 commits into
bendlang:mainfrom
sebastianbreguel:perf/call-spine-cons
Nov 2, 2026
Merged

nicolas-abril merged 2 commits into
bendlang:mainfrom
sebastianbreguel:perf/call-spine-cons

Conversation

@sebastianbreguel

@sebastianbreguel sebastianbreguel commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1956: the call spine and body_flatten. Constructor fields are left for later (see the end).

Checking a call f(a1, …, an) walks its nested Apps toward f, and each step built the pending arguments as a new array, [tm.x, ...sp], so a call with n arguments copied n(n-1)/2 entries. Flattening a def's n parameters did the same with body_flatten(b, vars.slice(1), fr).

Change (bend2/bend.ts only)

  • term_infer: the spine is a List<HTerm>, the file's existing cons list, and App passes list_set(sp, 0, tm.x) (a cons; the key is unused). The Ref case reads it into an array once, only when qt is not None, and def_inst, the slice(x) and the decreasing-call loop use that array as they used sp. No other caller passes a spine.
  • body_flatten, default arm: a loop from the last parameter to the first wraps the same Lams. The recursion always landed in this arm again, since b does not change.

No cache and no memo: it only drops copies (#993, #1075).

Measured on the issue's discard with n parameters and a call with n arguments, --check-only:

n spine entries copied, before after body_flatten entries copied, before after wall time, before after
2 1 2 1 0 18 ms 19 ms
8 28 8 28 0 19 ms 19 ms
32 496 32 496 0 20 ms 20 ms
512 130,816 512 130,816 0 25 ms 23 ms
2048 2,096,128 2,048 2,096,128 0 45 ms 32 ms
8192 33,550,336 8,192 33,550,336 0 262 ms 67 ms

Before is n(n-1)/2 in both places; after, the spine is read once (n entries) and body_flatten copies nothing. For n = 2 the spine read costs one entry more than before. Copies were counted with temporary counters, since removed; times are medians of 7 runs, before and after alternated.

Checker benches, medians of 5 alternated runs: compute_1600 501 → 503 ms, defs_12800 285 → 289, generics_3200 380 → 374, proofs_3200 817 → 777 (ranges 814–840 and 763–824 overlap; Linux below shows no change), trees_400 810 → 809. So this only matters for calls with many arguments.

On Linux, medians of 7: n = 512 27 → 23 ms, 2048 59 → 42 ms, 8192 381 → 104 ms; checker benches compute_1600 609 → 601, defs_12800 378 → 380, generics_3200 457 → 445, proofs_3200 931 → 926, trees_400 1159 → 1145.

Verification (macOS arm64, Bun 1.4.2; Linux arm64 (Docker, Debian 13, Bun 1.4.2, clang 19))

  • Copies in the spine and body_flatten, n = 2 to 8192, macOS: the table above
  • Wall time, issue program n = 2 to 8192, and the 5 checker benches, macOS and Linux: above
  • All 1796 tests under tests/, interpreter, before vs after, macOS and Linux: no difference (on macOS proof/app/main, which greets until killed, is not comparable; on Linux its output is capped at 1 MB and matches)
  • gates/repo.ts: 54/54 (bend.ts 44,532 / 48,000 ttok)
  • Strict tsc of bend.ts alone: the same 19 errors before and after, all missing types
  • gates/test.ts, gates/perf.ts: run on the minis
  • The constructor-field copies the issue also names (below)
  • Metal, CUDA: not run; the change is in the checker

Left out: the constructor-field copies the issue also names ([...xs, x] in lhs_ext, xs.concat([x]) in term_check_mat_goal) sit inside term bodies built as functions that can run more than once, so they need a different change. Hence "part of".

Note: #1734 reads sp[j] and sp.length in the Ref case; there the array is now xs, so whichever lands second needs a small rebase.

@YidaWeng

YidaWeng commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Verified against current main (00d29f44): --check-only passes; the cons-list spine preserves source argument order; body_flatten's loop is equivalent to the old recursion.

In 7 interleaved runs on a 2,048-argument call, this was consistently faster (~0.33–0.38s vs ~0.38–0.46s on main, startup included).

Two asks:

  1. Add a large-arity regression test outside bend.ts — 2,000 parameters and a 2,000-argument call would cover both loops.
  2. Coordinate with fix(check): a constructor's fields grow as a list, without copying #1966, which conflicts with this and with perf(check): ctx_dead searches the scope in place instead of copying it first #1965. I'd land perf(check): ctx_dead searches the scope in place instead of copying it first #1965 then this, and have fix(check): a constructor's fields grow as a list, without copying #1966 rebase/reduce afterward.

@sebastianbreguel

Copy link
Copy Markdown
Contributor Author

Thanks for checking it. Both asks:

  1. Added tests/check/large_arity.bend: a def with 2,000 parameters called with 2,000 arguments. It prints 10000 * p1 + p1999, so any reordering of the spine or the flattened parameters changes the output from 11999. It passes in the interpreter, the JS lane and the C lane. It also passes on main, so it guards the order and the arity rather than reproducing a failure.
  2. Agreed on perf(check): ctx_dead searches the scope in place instead of copying it first #1965 → perf(check): a call's pending arguments are a cons list, and parameters flatten in one loop #1967 → fix(check): a constructor's fields grow as a list, without copying #1966. If perf(check): ctx_dead searches the scope in place instead of copying it first #1965 lands first and this stops applying cleanly, I'll rebase it the same day.

nicolas-abril added a commit that referenced this pull request Nov 2, 2026
Level order changed which @unsafe binder type a program hangs on, and the
batched var columns saved about 0.02 s while dropping the error that guarded
them. #1965 fixes #1955 and #1967 fixes the call spine and body_flatten, so
this is now the fields #1956 still names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicolas-abril
nicolas-abril merged commit 01fca26 into bendlang:main Nov 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants