Skip to content

perf(js/ts): standard library one pass List.append builder - #5148

Open
shayanhabibi wants to merge 2 commits into
fable-compiler:mainfrom
shayanhabibi:perf/list-append
Open

shayanhabibi wants to merge 2 commits into
fable-compiler:mainfrom
shayanhabibi:perf/list-append

Conversation

@shayanhabibi

@shayanhabibi shayanhabibi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Source and proposed change

src/fable-library-ts/List.fs implements append as fold (...) ys (reverse xs). Each nonempty left node is copied into a reversed intermediate list, then copied again into the result. Python fable_library/list.fs and Dart List.fs use the same reverse/fold append but are not in scope for me.

Suggestion:

let append (xs: 'T list) (ys: 'T list) =
    let root = List.Empty
    let mutable node = root
    let mutable rest = xs
    while not rest.IsEmpty do
        node <- node.AppendConsNoTail rest.Head
        rest <- rest.Tail
    node.SetConsTail ys
    root.Tail
Semantic Proofs

What the compiler already knows

Full typed FCS context resolves the FSharp.Core ListModule.Append operation and immutable list types; no dynamic JS duck-typing test is needed. Replacements.fs:2427–2453 routes ListModule.Append through the generic List runtime call, while Replacements.fs:1672–1674 routes @ directly to List.append. The measured reverse/fold implementation is therefore still on these call paths; neither route already emits the forward builder. The useful static guarantee is immutable list spine plus fresh, unpublished construction nodes, not purity of the element type. Element values may be mutable and remain shared. This supports a runtime fix benefiting compiler-recognized call sites without adding a new AST construct or per-element dispatch.

Correctness and allocation

list-correctness.mjs: 48,337 successful assertions. Left lengths 0–150 crossed with right lengths 0, 1, 5, 50; output values, input nonmutation, element reference identity, exact right-tail identity, and empty-left identity checked. Instrumented actual FSharpList constructors confirm total node allocations of 2n+1 versus n+1 for n=0,1,8,128,4096 (counts exclude input construction). This is allocation-count evidence, not measured bytes, retained heap, or GC time.

No user callback is involved. Elements are copied as references and never compared, hashed, coerced, or enumerated individually; mutation of an element remains observable through both lists. Input nodes remain untouched. A fresh result prefix ends in the original right list, including the empty-left case. Valid F# immutable lists are the boundary: JS proxies or accessors replacing node fields, cyclic/corrupt lists, monkey-patched runtime functions, and allocation failure can observe different traversal/allocation behavior and are not covered. No .NET fixture or full compiler/test-suite run was performed for this experiment.

Timing

Windows, Node v26.7.0. Two exclusive runs; 9 alternating ABBA/BAAB blocks per case, 20 ms per sample, two samples per variant/block, 40 ms warmup per variant, explicit GC before blocks. Outputs escape to a global sink. Input generation excluded. Median paired candidate time change (negative is faster):

Left length Primary Repeat
0 +4.7% -4.2%
1 -57.2% -60.5%
8 -76.0% -75.9%
32 -80.4% -81.7%
128 -81.5% -79.6%
1024 -75.8% -78.7%
8192 -77.7% -80.4%

Absolute baseline times drifted between runs (e.g. n=128 about 2843 ns versus 5574 ns); do not present pooled ratios or precision estimates. Relative direction is strong for nonempty inputs; empty case is inconclusive. Microbenchmarks do not establish application or browser improvements. Runtime changes affect all JS/TS List.append calls and internal callers, with a small code surface but broad usage.

Warning

Before rollout we should test select applications, or save this for a pre-release

This branch has not been deployed

No deployments
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.

1 participant