Skip to content

perf(c): build lists directly from flat arrays - #2012

Draft
vicmcorrea wants to merge 2 commits into
bendlang:mainfrom
vicmcorrea:perf/c-array-to-list-20261102
Draft

vicmcorrea wants to merge 2 commits into
bendlang:mainfrom
vicmcorrea:perf/c-array-to-list-20261102

Conversation

@vicmcorrea

@vicmcorrea vicmcorrea commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem and fix

Array.to_list recursively splits native flat storage, allocating and copying halves before building the list. This implements a C lowering for checked instances of Base's Array.to_list.go, using nuxyel's workload from #1991.

Walk cells backward into the supplied suffix, transferring owned values or retaining shared boxed fields, then release the source block. Reuse the existing layouts, constructor builder, sharing facts and device error poll.

Six alternating one-thread runs per version of the reported 64 × 2²⁰ F32 fill/convert/sum workload, macOS M1 Pro, clang 23.1.4 -O3:

Median Main db64cf7 This PR
CPU user time 0.78 s 0.27 s
Retired instructions 9.82 billion 2.02 billion

Every run prints 23.789732. These measure the whole workload, including allocation and summation. Wall time varies with host activity.

Blast radius

The change adds 23 lines in bend2/comp.ts and two compiler fixtures. Only Base's recorded helper specializations receive this C lowering. Base definitions and the JS reference runtime are unchanged. No-use controls emit byte-identical C, including device input.

There is no public API change. The source block stays live until the list is built. For an owned singleton with two Nat fields, the observed class-1 order changes from free, allocate, allocate to allocate, allocate, free, briefly retaining two extra words. Ten million such conversions retired 5% more instructions, with median CPU time 0.16 → 0.14 s.

Regression evidence

Fixtures cover order, packed and padded values, shared boxed ownership, affine closures, partial application, Unit, a supplied suffix, and conversions in the leaves of a banged fork tree. Behavioral output agrees with main. Removing the shared Shape-field retain produces a memory fault in all three poison runs. The intact implementation passes.

Actual heap hooks on eight F32 cells observe 12 class-0 and 10 class-1 allocations on main, versus eight class-1 list allocations here. The new trace frees the original class-2 block after those list nodes; it does not allocate intermediate array halves.

Checks

Validation limits

Compiler budget blocks merge: the submitted head is 66,372 / 66,000 ttok, so hosted CI remains 53/54. A private combination of the unchanged patch with main 0ba4e5e is 66,277 / 66,000; the new upstream cleanup still leaves 277 tokens over the cap. No cap changes or unrelated trimming are included, following the scoped-feature guidance in #1820.

The full test gate cannot reach the unresolved cluster hostname. Local execution used macOS. Linux and CUDA evidence above is contributor-reported. Windows, concurrent alias mutation and dedicated leak stress remain unqualified. Typechecking matched the seven diagnostics of the tested db64cf7 baseline in the protected checker and documentation tools, with none in comp.ts.

The issue reporter confirmed this implements the intended flat walk and will not open a competing PR. The earlier JS flat-walk proposal was withdrawn.

Pending #2017 changes the same c_units branch and would require a textual resolution if it lands first. Its HID feature is separate from this lowering. Pending #2019 introduces host array views and explicitly leaves #1991 to a flat walk.

@nuxyel

nuxyel commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Thanks for picking this up, and for crediting the report. I won't open a second PR for #1991; this one is the flat walk I had in mind.

You listed Linux and CUDA as not qualified, so I ran the PR head (f27fd28) against its base (db64cf7) here: Core Ultra 7 155H, Linux, clang 23.1.3, RTX 4050 with CUDA 13.4.

  • Your two fixtures and the two existing consumers (tests/base/map_set_array_kit, tests/run/array_map_loop) print their #| lines on C, on JS (bun) and on C built with -DBEND_POISON (three runs each), with both compilers. array_to_list_device also passes with its bang on the RTX 4050 (+RTS --gpu on) and on the CPU threads. The emitted C differs from the base's in all four files, so the new path is the one that ran, on the device too.
  • C backend: Base's Array.to_list copies both halves at every level, ~190 instructions a cell (a loop of Array.get takes ~35) #1991's repro, one thread pinned with taskset -c 3, +RTS --threads 1 -RTS. Instructions (instructions:u) from three rounds; time is the whole process, median and range of five alternated runs on an idle machine:
main db64cf7 this PR
Array.to_list, instructions 12,825.56 M 2,251.43 M
Array.to_list, time 0.894 s (0.887-0.906) 0.261 s (0.254-0.265)
loop of Array.get (control), instructions 2,318.55 M 2,318.55 M
loop of Array.get (control), time 0.254 s (0.250-0.277) 0.260 s (0.256-0.263)

All print 23.789732. So on x86 it's 5.7x fewer instructions and 3.4x less time (cycles go from 3,972 M to 1,118 M), and Array.to_list now takes about as long as the hand-written loop, with slightly fewer instructions. The control loop doesn't move.

One thing to be upfront about: my own packages don't call Array.to_list (tensor-array reads arrays back with a get loop), so I don't have a whole-program number for this, only the conversion itself.

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