pv: build the vector trie bottom-up, not n incremental conjs (jolt-5vsp) (#153)

pv-from-indexed (behind vec/mapv/filterv/into-a-vector) built a pvec by calling
pv-conj once per element — each call allocated a fresh pvec wrapper and copied
the up-to-32 tail tuple, so building an n-vector was O(n) allocations + tail
copies. Replace it with a single bottom-up trie construction: chunk the elements
into 32-wide value leaves, group nodes 32-wide up to the root, split off the
tail. The structure is identical to the incremental one — tail-offset(n) =
((n-1)>>5)<<5 is exactly the trie/tail boundary, so nth/conj/assoc/seq read it
unchanged (validated against the old builder across the size boundaries).

into-a-vector likewise stops doing a persistent pv-conj per element: it
accumulates into a native array and bulk-builds once (the transient-style path).

Measured (50k): vec 211 -> 6 ms (~36x), into [] 197 -> 15 ms (~13x). mapv is
unchanged here — it's bottlenecked on lazy map realization, not the build.

The map/set builders (into {}, frequencies, group-by, set — all HAMT-backed)
need the same bulk treatment and are a separate follow-up. Gate: conformance x3,
full suite, new bulk-boundary rows in vectors-spec.

Co-authored-by: Yogthos <yogthos@gmail.com>
This commit is contained in:
Dmitri Sotnikov 2026-06-16 21:31:26 +00:00 committed by GitHub
parent b771908e5b
commit 43e5426601
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 6 deletions

View file

@ -52,3 +52,18 @@
["large vector nth" "1500" "(nth (vec (range 2000)) 1500)"]
["large vector count" "2000" "(count (vec (range 2000)))"]
["large conj immutable" "true" "(let [v (vec (range 1000)) w (conj v :end)] (and (= 1000 (count v)) (= 1001 (count w))))"])
# vec/into build the pvec trie BOTTOM-UP in one pass (pv-from-indexed, jolt-5vsp
# collections). Exercise the structure transitions: 32 (tail full), 33 (first
# trie leaf), 1024 (root full at shift 5), 1025 (root grows to shift 10) — and
# that a conj/assoc after a bulk build still lands and reads back.
(defspec "vector / bulk build boundaries"
["count at 1025" "1025" "(count (vec (range 1025)))"]
["into = vec at 1025" "true" "(= (vec (range 1025)) (into [] (range 1025)))"]
["nth at leaf boundary" "32" "(nth (vec (range 1025)) 32)"]
["nth at root boundary" "1024" "(nth (vec (range 1025)) 1024)"]
["vec=into at 33" "true" "(= (vec (range 33)) (into [] (range 33)))"]
["conj after bulk 1024" "1025" "(count (conj (vec (range 1024)) :x))"]
["conj-after reads back" ":x" "(nth (conj (vec (range 1024)) :x) 1024)"]
["assoc into bulk vec" "9" "(nth (assoc (vec (range 1025)) 1000 9) 1000)"]
["into onto non-empty" "[0 1 2 0 1]" "(into (vec (range 3)) (range 2))"])