Repository navigation
perf(c): blk_new fills whole elements, clones keep runs; JS shares the cell depth wall - #1946
Open
nicolas-abril wants to merge 3 commits into
Open
nicolas-abril wants to merge 3 commits into
nicolas-abril wants to merge 3 commits into
Conversation
blk_new's fill now loops over elements and, inside, over the 2^lgs cells of one, so clang vectorizes multi-cell elements (Pt, P3, structs) as it already did U32. The per-cell `i % (1u << lgs)` and `< n` test are gone. Two error paths in blk_new, both device-only in effect (the host exits on err_post): - the depth check is `d > 31 - lgs`, which cannot wrap; - once ERR_CNTS is posted the element is not kept, so a 2^d-1 bump no longer carries out of the 24-bit count into the loc bits. The JS array_new took only the depth, so [p : Pt^31n] tried to build 2^31 elements where C refuses class 32. The JS emitter now passes the element's cell class and array_new refuses d > 31 - lgs, as C does; tests/io/array_depth_bound_wide.bend pins it. To fit the cap, the JS array templates share array_pair and lay_arr/arr_new say `n || 1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copy that keeps its cells (Array.clone of boxed elements, and node or
half of a shared block) made one atomic count add per cell. blk_fill now
copies the block in one plain pass, which vectorizes, then walks the
copy and keeps each run of k equal cells with one bump of k, the way
blk_new keeps each field once by 2^d - 1. k cells hold k counts, so a
run stays under RFC_CNT and rfc_bump still sees an overflow.
The keep pass reads the copy, not the source, so each cell keeps the
term it was copied as even while another thread wraps the shared source
in place. tests/reg/array_clone_runs.bend reads back every cell of both
copies of String and {String, U32} clones with long and short runs.
comp.ts goes past its 66k ttok cap (66,124).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nicolas-abril
force-pushed
the
perf/blk-new-element-fill
branch
from
October 30, 2026 17:15
4ea590b to
9a60419
Compare
array_pair wasn't only about arrays once nat_divmod and io_tup could use it, so it is tup_new now, beside cmp_new. blk_fill declares v where it's assigned and passes k without a cast. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Follow-up to #1928. That PR gave clang a trip count for
blk_new's fill, which vectorizedU32fills; elements of two or more cells stayed scalar. This PR fills element by element so they vectorize too, fixes twoblk_newerror paths, keeps a clone's runs of equal cells with one refcount bump each, and makes the JS lanes refuse the same array depths as C.comp.tsis over its cap: 66,075 > 66,000 ttok, sogates/repo.tsfails (53 / 54). This was knowingly left for now.Fill
The inner count is the constant
2^lgsat every inlined call, so clang unrolls it and stores whole elements in vectors. The per-celli % (1u << lgs)and< ntest are gone. The index isu32:c <= 31, sow <= 2^31.Telling clang
c <= 31(__builtin_assume, orc & 31) does not vectorize the oldi < (1ull << c)loop. Only the!=form does.Clone
A copy that keeps its cells (
Array.cloneof boxed elements, andblk_node/blk_halfon a shared block) made one atomic count add per cell throughblk_keep.blk_fillnow copies the block in one plain pass, which vectorizes. It then walks the copy and keeps each run ofkequal cells with oneterm_keep(e, w, k), the wayblk_newkeeps each field once by2^d - 1.kcells holdkcounts, sokstays underRFC_CNTandrfc_bumpstill sees an overflow: a 2^23-cellStringclone still fails with "more than 2^24-1 live copies", and a 2^22 one still runs.The keep pass reads the copy, not the source. A first version kept from the source after the copy, so a cell that another thread wrapped in place in between would keep the wrapped term while the copy held the raw one. Reading the copy keeps exactly the term each cell was copied as, as the old per-cell
blk_keepdid. A wrap (k = 1, an unshared term) is still written back to the source as before.Error paths (in effect only on the device, where
err_postdoes not exit)d > 31 - lgs(lgs <= 31), so it cannot wrap. Runtime Nats stop at 2^48-1, so the oldd + lgs > 31could not wrap either; the new form does not depend on that cap.ERR_CNTSis posted, the element is no longer kept withk = 2^d - 1. That bump carried out of the 24-bit count into the loc bits of the redirect cell.JS depth wall
array_newin JS checked onlyd > 31, so[p : Pt^31n]set out to build 2^31 elements (still running after 5 minutes) while C refuses class 32 with "an array past the deepest block class 31". The JS emitter now passes the element's cell class (lay_arr(lay_el(T)).lgs, the value C gets) toarray_new, which refusesd > 31 - lgs. C, JS and the interpreter now refusePt^31n,P3^30nandU32^32nalike.tests/io/array_depth_bound_wide.bendpins thePtcase. It passes on both lanes here and times out on main's JS lane.To pay for the JS change, every tuple the JS runtime builds (the array templates,
array_rmw,nat_divmodandio_tup) goes through onetup_new(a, b)helper, andlay_arr/arr_newsayn || 1forMath.max(1, n).Benches
Apple Silicon, Apple clang 17,
clang -std=c11 -O3as the CLI builds. Each probe makes 100 arrays (fresh, or grown by doubling from one leaf as in #1928) and reads one cell of each into a checksum. Cycles are from/usr/bin/time -l, medians of three. Every probe prints the same checksum before and after. Most of what is left in the clone probes is dropping the clone (term_dropwalks every cell), which this does not change.These numbers are from before the rebase on f1273d3 and weren't run again.
Array<U32>doubled to 2^24Array<Pt>(2 ×U32) doubled to 2^23Array<P3>(3 ×U32) doubled to 2^22Array<String>doubled to 2^20Array.new(Pt, 23n, v)Array.new(P3, 22n, v)Array.new(String, 20n, v)Array.new(String, 20n, v)Checks
bun gates/repo.ts: 53 / 54.comp.tsis 66,075 ttok, over its 66k cap (main is 65,960).bun gates/test.tson the minis, rebased on f1273d3: 1800 / 1800.gfx/windowandgfx/clicksexpect "no display", andio/stack_fault_trapexits 138 under the runner where the test expects 1.io/chan_reclaim_limitfailed once on main's JS lane and passed on rerun (timing).U32,Pt,P3,String,{String, U32}and{U32, String, U32}, made fresh and by doubling. C, JS and the interpreter print the same value.ERR_CNTS(Array.new(String, 24n, s)) andERR_ARRSstill fire on the host.tests/reg/array_clone_runs.bendclonesStringand{String, U32}arrays with one long run and with runs broken up byArray.set, writes one copy, and sums every cell of both, depths 0–11. C, JS and the interpreter agree, and so does main's C. With the run bump forced to 1 it ends in a memory fault. An overcount only leaks, which it cannot see.Not covered
stencil3dbuilds with Metal and passes, which only shows that the runtime compiles there.gates/perf.ts. No bench pins the multi-cell fill.tup_new(one small call per get, size, clone and divmod, which the JIT should inline). Not measured.ANodeof two class-31 blocks: C refuses class 32, while JS concatenates. It needs more than 2^31 cells to reach.{String, U32}, interleave their cells, so their clones have runs of one and gain only from the plain copy pass.blk_node/blk_halfon a shared block take the new keep path too. Only the suite and the readback test cover them, not a bench.🤖 Generated with Claude Code