Skip to content

perf(c): fill blk_new by element stride, not per-cell modulo - #1938

Closed
YidaWeng wants to merge 1 commit into
bendlang:mainfrom
YidaWeng:perf/blk-new-multifield
Closed

YidaWeng wants to merge 1 commit into
bendlang:mainfrom
YidaWeng:perf/blk-new-multifield

Conversation

@YidaWeng

Copy link
Copy Markdown
Contributor

Draft — coordinating with @raramos9 on #1890 / #1928. Happy to adjust or stand down if that follow-on is already theirs.

Part of #1890. Related to #1928 (trip-count fix for runtime-depth U32 fills). This PR targets the multi-word case #1928 left open, and keeps the w = 1ull << c; i != w trip-count form in the new loop.

Cause

blk_new filled with

for (u64 i = 0; i < (1ull << c); i += 1) {
  blk_write(..., i % (1u << lgs) < n ? v[i % (1u << lgs)] : 0);
}

For U32 (lgs == 0) that is one invariant store. For a two-field struct (lgs == 1, n == 2) it reloads v[i % 2] every cell, so multi-word growth stays heavier than the per-word U32 rate even after #1928's trip-count fix.

Change

Walk the block by element stride instead of by cell with modulo:

-  for (u64 i = 0; i < (1ull << c); i += 1) {
-    blk_write(H, arr, l, (u32)i, i % (1u << lgs) < n ? v[i % (1u << lgs)] : 0);
+  for (u64 i = 0, w = 1ull << c; i != w; ) {
+    for (u32 j = 0; j < (1u << lgs); j += 1, i += 1) {
+      blk_write(H, arr, l, (u32)i, j < n ? v[j] : 0);
+    }
   }

w is a multiple of 1 << lgs (c = d + lgs), so the loops cover exactly the block. Padding when n < stride stays a zero write. The common Array<Pt> shape unrolls to two loop-invariant stores. The outer i != w form keeps #1928's clang-readable trip count.

Benches

Linux x86_64, Bend built with the CLI's clang -std=c11 -O3, compared against main plus the #1928 one-line trip-count fix as the baseline for fairness on multi-field (that fix alone does not close the Pt gap). Each probe makes 100 arrays and folds one cell of each into a checksum. Instructions from perf stat -e instructions:u (medians of three), minus the same probe at depth 0, divided by 100 × 2^d. Checksums match before/after, and match the JS lane.

probe baseline (#1928 trip-count), instr / element this PR change
Array<Pt> (two U32 fields) doubled to 2^23 8.00 5.25 −34%
Array<U32> doubled to 2^24 2.63 2.63 none

5.25 instr/element for Pt is 2.625 instr/word — the same per-word rate as U32 on this host. Wall clock here is noisy (memory-bound); instructions are the stable signal.

Checks

  • bun gates/repo.ts: 54 / 54. comp.ts is 65,973 ttok (gpt-3.5-turbo), under the 66,000 cap.
  • C-lane #| match on: array_fill_fold, array_boxed_get, array_size, array_poly_elem, array_map_loop, array_fork, array_copy_pair, array_nested, array_clone_boxed, array_static_default, array_equality.
  • Probe checksum identical on C and JS.
  • Cluster gates not run (no mini access). GPU / CUDA: Array.new inside a bang fills the new block in one loop on one lane (~4.6 ns a cell) #1778 not addressed. Proof-backed init not addressed.

blk_new wrote each cell with i % (1<<lgs), which reloads v[i%stride]
every iteration. For multi-word elements (lgs>=1) that kept the fill
scalar on top of bendlang#1928's trip-count fix.

Walk the block by element instead: an inner loop over the stride writes
v[j] (or 0 when padding). The common Array<Pt> case (lgs==1, n==2)
unrolls to two invariant stores. On this Linux x86_64 host, doubling
Array<Pt> to 2^23 goes from 8.00 to 5.25 instructions per element
(matching U32's per-word rate); U32 doubling is unchanged.

Part of bendlang#1890. Builds on bendlang#1928.
@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks for this, and for claiming the slice cleanly in #1890. The diagnosis and the instruction counts are right.

We're closing this in favor of #1946. It makes the same change to blk_new's fill: it walks whole elements with an inner loop over the stride and drops the per-cell i % (1 << lgs). It also covers the rest of the area:

  • it fixes two blk_new error paths: the depth check d > 31 - lgs, and a skipped keep after ERR_CNTS;
  • it keeps each run of equal cells in a clone with one refcount bump;
  • it makes the JS lanes refuse the same array depths as C;
  • it adds tests.

Both PRs rewrite the same loop, so only one can land. #1938 would also need a rebase onto #1928.

@YidaWeng
YidaWeng deleted the perf/blk-new-multifield branch October 30, 2026 11:32
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.

2 participants