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:
parent
b771908e5b
commit
43e5426601
3 changed files with 51 additions and 6 deletions
|
|
@ -427,7 +427,12 @@
|
|||
(do (var result to)
|
||||
(each item items (set result (core-assoc result (vnth item 0) (vnth item 1))))
|
||||
result)
|
||||
(pvec? to) (do (var result to) (each x items (set result (pv-conj result x))) result)
|
||||
# Accumulate into a native array and bulk-build the pvec ONCE (pv-from-indexed),
|
||||
# instead of an immutable pv-conj per element (each allocating a wrapper +
|
||||
# copying the tail). This is the transient-style fast path for into-a-vector.
|
||||
(pvec? to) (let [arr (array/slice (pv->array to))]
|
||||
(each x items (array/push arr x))
|
||||
(make-vec arr))
|
||||
(array? to) (if mutable?
|
||||
(do (each x items (array/push to x)) to) # vector: append
|
||||
(do (var result (array/slice to)) (each x items (array/insert result 0 x)) result)) # list: prepend
|
||||
|
|
|
|||
|
|
@ -156,9 +156,34 @@
|
|||
out)
|
||||
|
||||
(defn pv-from-indexed [xs]
|
||||
# Build a pvec from any Janet-indexed collection (tuple/array).
|
||||
(var pv EMPTY)
|
||||
# Build a pvec from any Janet-indexed collection (tuple/array) by constructing
|
||||
# the trie BOTTOM-UP in one pass, instead of n incremental pv-conj calls (each
|
||||
# of which allocated a pv wrapper and copied the tail). The structure produced
|
||||
# is identical to the incremental one — tail-offset(n) = ((n-1)>>5)<<5 is
|
||||
# exactly the trie/tail split, so leaf-for/nth/conj/assoc all read it the same.
|
||||
# This is the bulk path behind vec/mapv/into-a-vector (jolt-5vsp collections).
|
||||
(def n (length xs))
|
||||
(var i 0)
|
||||
(while (< i n) (set pv (pv-conj pv (in xs i))) (++ i))
|
||||
pv)
|
||||
(if (<= n width)
|
||||
# everything fits in the tail (matches EMPTY's shift)
|
||||
(make-pv n bits empty-node (tuple/slice xs))
|
||||
(let [tail-len (+ 1 (mod (- n 1) width))
|
||||
trie-count (- n tail-len) # = tail-offset(n), a multiple of width
|
||||
tail (tuple/slice xs trie-count n)]
|
||||
# value leaves: full 32-element tuples over [0, trie-count)
|
||||
(def leaves @[])
|
||||
(var i 0)
|
||||
(while (< i trie-count)
|
||||
(array/push leaves (tuple/slice xs i (+ i width)))
|
||||
(set i (+ i width)))
|
||||
# group nodes 32-wide, bottom-up, until <=32 remain; that becomes the root
|
||||
(var level leaves)
|
||||
(var shift bits)
|
||||
(while (> (length level) width)
|
||||
(def parent @[])
|
||||
(var j 0)
|
||||
(while (< j (length level))
|
||||
(array/push parent (tuple/slice level j (min (length level) (+ j width))))
|
||||
(set j (+ j width)))
|
||||
(set level parent)
|
||||
(set shift (+ shift bits)))
|
||||
(make-pv n shift (tuple/slice level) tail))))
|
||||
|
|
|
|||
|
|
@ -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))"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue