Skip to content

Commit 3a59e44

Browse files
committed
Improve performance of removing elements from sequences
Merging `InsertBeforeIndex` protocol into new `IndexedOps` protocol for optimized indexed based operations (which now includes inserting before and removing at) Updating `nthpath` and `keypath` to use new helper fns Adding tests and benchmarks
1 parent e8225f0 commit 3a59e44

File tree

3 files changed

+119
-35
lines changed

3 files changed

+119
-35
lines changed

scripts/benchmarks.clj

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@
112112
(run-benchmark "transform values of a list"
113113
(transform ALL inc data)
114114
(doall (sequence (map inc) data))
115-
(reverse (into '() (map inc) data))
116-
))
115+
(reverse (into '() (map inc) data))))
116+
117117

118118
(let [data {:a 1 :b 2 :c 3 :d 4}]
119119
(run-benchmark "transform values of a small map"
@@ -127,8 +127,8 @@
127127
(into {} (map (fn [e] [(key e) (inc (val e))]) data))
128128
(into {} (map (fn [e] [(key e) (inc (val e))])) data)
129129
(map-vals-map-iterable data inc)
130-
(map-vals-map-iterable-transient data inc)
131-
))
130+
(map-vals-map-iterable-transient data inc)))
131+
132132

133133

134134
(let [data (->> (for [i (range 1000)] [i i]) (into {}))]
@@ -152,8 +152,8 @@
152152
(first data)
153153
(select-any ALL data)
154154
(select-any FIRST data)
155-
(select-first ALL data)
156-
))
155+
(select-first ALL data)))
156+
157157

158158
(let [data [1 2 3 4 5]]
159159
(run-benchmark "map a function over a vector"
@@ -192,8 +192,8 @@
192192
(run-benchmark "prepend to a vector"
193193
(vec (cons 0 data))
194194
(setval BEFORE-ELEM 0 data)
195-
(into [0] data)
196-
))
195+
(into [0] data)))
196+
197197

198198
(declarepath TreeValues)
199199

@@ -314,8 +314,8 @@
314314
(map (fn [[k v]] [(keyword (str *ns*) (name k)) v]))
315315
data)
316316
(reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data)
317-
(setval [MAP-KEYS NAMESPACE] (str *ns*) data)
318-
))
317+
(setval [MAP-KEYS NAMESPACE] (str *ns*) data)))
318+
319319

320320

321321
(let [data (->> (for [i (range 1000)] [(keyword (str i)) i]) (into {}))]
@@ -324,8 +324,8 @@
324324
(map (fn [[k v]] [(keyword (str *ns*) (name k)) v]))
325325
data)
326326
(reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data)
327-
(setval [MAP-KEYS NAMESPACE] (str *ns*) data)
328-
))
327+
(setval [MAP-KEYS NAMESPACE] (str *ns*) data)))
328+
329329

330330
(defnav walker-old [afn]
331331
(select* [this structure next-fn]
@@ -336,8 +336,8 @@
336336
(let [data {:a [1 2 {:c '(3 4) :d {:e [1 2 3] 7 8 9 10}}]}]
337337
(run-benchmark "walker vs. clojure.walk version"
338338
(transform (walker number?) inc data)
339-
(transform (walker-old number?) inc data)
340-
))
339+
(transform (walker-old number?) inc data)))
340+
341341

342342
(let [size 1000
343343
middle-idx (/ size 2)
@@ -354,4 +354,29 @@
354354
(run-benchmark "before-index at 0 vs. srange vs. cons (list)"
355355
(setval (before-index 0) v data-lst)
356356
(setval (srange 0 0) [v] data-lst)
357-
(cons v data-lst)))
357+
(cons v data-lst))
358+
(run-benchmark "set keypath and nthpath at index to NONE versus srange in middle (vector)"
359+
(setval (nthpath middle-idx) NONE data-vec)
360+
(setval (keypath middle-idx) NONE data-vec)
361+
(setval (srange middle-idx (inc middle-idx)) [] data-vec))
362+
(run-benchmark "set keypath and nthpath at index to NONE versus srange in middle (list)"
363+
;; this case still needs to be optimized in nthpath*
364+
(setval (nthpath middle-idx) NONE data-lst)
365+
(setval (keypath middle-idx) NONE data-lst)
366+
(setval (srange middle-idx (inc middle-idx)) [] data-lst))
367+
(run-benchmark "set keypath and nthpath at beginning to NONE versus srange and subvec (vector)"
368+
(setval (nthpath 0) NONE data-vec)
369+
(setval (keypath 0) NONE data-vec)
370+
(setval (srange 0 1) [] data-vec)
371+
(subvec data-vec 1))
372+
(run-benchmark "set keypath and nthpath at beginning to NONE versus srange and rest (list)"
373+
;; this case still needs to be optimized in nthpath*
374+
(setval (nthpath 0) NONE data-lst)
375+
(setval (keypath 0) NONE data-lst)
376+
(setval (srange 0 1) [] data-lst)
377+
(rest data-lst))
378+
(run-benchmark "set keypath and nthpath at end to NONE versus srange and subvec (vector)"
379+
(setval (nthpath (dec size)) NONE data-vec)
380+
(setval (keypath (dec size)) NONE data-vec)
381+
(setval (srange (dec size) size) [] data-vec)
382+
(subvec data-vec 0 (dec size))))

src/clj/com/rpl/specter/navs.cljc

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -492,9 +492,6 @@
492492
(defprotocol FastEmpty
493493
(fast-empty? [s]))
494494

495-
(defprotocol InsertBeforeIndex
496-
(insert-before-idx [aseq idx val]))
497-
498495
(defnav PosNavigator [getter updater]
499496
(select* [this structure next-fn]
500497
(if-not (fast-empty? structure)
@@ -643,7 +640,29 @@
643640
(nth s (-> s count dec))
644641
))
645642

643+
(defprotocol IndexedOps
644+
"Fast indexed operations on sequential types"
645+
(insert-before-idx [aseq idx val])
646+
(remove-at-idx [aseq idx]))
647+
648+
;; helper fns for indexed operations
649+
(defn- insert-before-index-list [lst idx v]
650+
;; an implementation that is most efficient for list style structures
651+
(let [[front back] (split-at idx lst)]
652+
(concat front (cons v back))))
653+
654+
(defn- remove-at-index-vec [aseq idx]
655+
(condp = idx
656+
0 (subvec aseq 1)
657+
(vec-count aseq) (subvec aseq 0 (vec-count aseq))
658+
(into (subvec aseq 0 idx) (subvec aseq (inc idx)))))
646659

660+
(defn- remove-at-index-list [lst idx]
661+
;; an implementation that is most efficient for list style structures
662+
(condp = idx
663+
0 (rest lst)
664+
(let [[front back] (split-at idx lst)]
665+
(concat front (rest back)))))
647666

648667
(extend-protocol FastEmpty
649668
nil
@@ -664,7 +683,7 @@
664683
(let [newv (next-fn vals (get structure key))]
665684
(if (identical? newv i/NONE)
666685
(if (sequential? structure)
667-
(i/srange-transform* structure key (inc key) (fn [_] []))
686+
(remove-at-idx structure key)
668687
(dissoc structure key))
669688
(assoc structure key newv))))
670689

@@ -704,8 +723,8 @@
704723
(if (vector? structure)
705724
(let [newv (next-fn vals (nth structure i))]
706725
(if (identical? newv i/NONE)
707-
(i/srange-transform* structure i (inc i) (fn [_] []))
708-
(assoc structure i newv)))
726+
(remove-at-index-vec structure i)
727+
(assoc structure i newv)))
709728
(i/srange-transform* ; can make this much more efficient with alternate impl
710729
structure
711730
i
@@ -726,35 +745,60 @@
726745
(end-fn structure)
727746
))
728747

729-
(defn- insert-before-index-list [lst idx val]
730-
;; an implementation that is most efficient for list style structures
731-
(let [[front back] (split-at idx lst)]
732-
(concat front (cons val back))))
733-
734-
(extend-protocol InsertBeforeIndex
748+
(extend-protocol IndexedOps
735749
nil
736750
(insert-before-idx [_ idx val]
737751
(if (= 0 idx)
738752
(list val)
739753
(throw (ex-info "For a nil structure, can only insert before index 0"
740754
{:insertion-index idx}))))
755+
(remove-at-idx [_ _]
756+
;; removing from nil structure at any index should just be nil?
757+
nil)
741758

742759
#?(:clj java.lang.String :cljs string)
743-
(insert-before-idx [aseq idx val]
744-
(apply str (insert-before-index-list aseq idx val)))
760+
(insert-before-idx [aseq idx v]
761+
(apply str (insert-before-index-list aseq idx v)))
762+
(remove-at-idx [s idx]
763+
(str (subs s 0 idx) (subs s idx)))
745764

746765
#?(:clj clojure.lang.LazySeq :cljs cljs.core/LazySeq)
747-
(insert-before-idx [aseq idx val]
748-
(insert-before-index-list aseq idx val))
766+
(insert-before-idx [aseq idx v]
767+
(insert-before-index-list aseq idx v))
768+
(remove-at-idx [aseq idx]
769+
(remove-at-index-list aseq idx))
749770

750771
#?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector)
751772
(insert-before-idx [aseq idx val]
752773
(let [front (subvec aseq 0 idx)
753774
back (subvec aseq idx)]
754775
(into (conj front val) back)))
776+
(remove-at-idx [aseq idx]
777+
(remove-at-index-vec aseq idx))
778+
779+
;; TODO: incorporate this into the transients namespace instead/in addition to?
780+
#?(:clj clojure.lang.ITransientVector :cljs cljs.core/TransientVector)
781+
(insert-before-idx [aseq idx val]
782+
(loop [v aseq prev-val val curr-idx idx]
783+
(if
784+
(= curr-idx (.count v))
785+
(assoc! v curr-idx prev-val)
786+
(let [curr-val (nth v curr-idx)]
787+
(recur (assoc! v curr-idx prev-val) curr-val (inc curr-idx))))))
788+
(remove-at-idx [aseq idx]
789+
(loop [v aseq curr-idx idx]
790+
(if
791+
(< curr-idx (dec (.count v)))
792+
(let [next-val (nth v (inc curr-idx))]
793+
(recur (assoc! v curr-idx next-val) (inc curr-idx)))
794+
(pop! v))))
755795

756796
#?(:clj clojure.lang.IPersistentList :cljs cljs.core/List)
757797
(insert-before-idx [aseq idx val]
758798
(cond (= idx 0)
759-
(cons val aseq)
760-
:else (insert-before-index-list aseq idx val))))
799+
(update-first aseq val)
800+
:else (insert-before-index-list aseq idx val)))
801+
(remove-at-idx [aseq idx]
802+
(if (= idx 0)
803+
(rest aseq)
804+
(remove-at-index-list aseq idx))))

test/com/rpl/specter/core_test.cljc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(ns com.rpl.specter.core-test
22
#?(:cljs (:require-macros
3-
[cljs.test :refer [is deftest]]
3+
[cljs.test :refer [is deftest testing]]
44
[clojure.test.check.clojure-test :refer [defspec]]
55
[com.rpl.specter.cljs-test-helpers :refer [for-all+]]
66
[com.rpl.specter.test-helpers :refer [ic-test]]
@@ -13,7 +13,7 @@
1313
defdynamicnav traverse-all satisfies-protpath? end-fn
1414
vtransform]]))
1515
(:use
16-
#?(:clj [clojure.test :only [deftest is]])
16+
#?(:clj [clojure.test :only [deftest is testing]])
1717
#?(:clj [clojure.test.check.clojure-test :only [defspec]])
1818
#?(:clj [com.rpl.specter.test-helpers :only [for-all+ ic-test]])
1919
#?(:clj [com.rpl.specter
@@ -33,6 +33,7 @@
3333
#?(:cljs [clojure.test.check.generators :as gen])
3434
#?(:cljs [clojure.test.check.properties :as prop :include-macros true])
3535
[com.rpl.specter :as s]
36+
[com.rpl.specter.navs :as n]
3637
[com.rpl.specter.transients :as t]
3738
[clojure.set :as set]))
3839

@@ -1331,6 +1332,7 @@
13311332
(deftest nthpath-test
13321333
(is (predand= vector? [1 2 -3 4] (transform (s/nthpath 2) - [1 2 3 4])))
13331334
(is (predand= vector? [1 2 4] (setval (s/nthpath 2) s/NONE [1 2 3 4])))
1335+
(is (predand= vector? [1 2 3] (setval (s/nthpath 3) s/NONE [1 2 3 4])))
13341336
(is (predand= (complement vector?) '(1 -2 3 4) (transform (s/nthpath 1) - '(1 2 3 4))))
13351337
(is (predand= (complement vector?) '(1 2 4) (setval (s/nthpath 2) s/NONE '(1 2 3 4))))
13361338
(is (= [0 1 [2 4 4]] (transform (s/nthpath 2 1) inc [0 1 [2 3 4]])))
@@ -1702,3 +1704,16 @@
17021704
(is (satisfies-protpath? FooPP "a"))
17031705
(is (not (satisfies-protpath? FooPP 1)))
17041706
)))
1707+
1708+
;; adding a separate test because these are not yet exercised by the rest of the suite
1709+
(deftest indexed-opts-transient-vectors-test
1710+
(testing "IndexedOps fns work properly for transient vectors"
1711+
(let [v (vec (range 10))]
1712+
(doseq [[f args exp] [[n/remove-at-idx [0] (vec (range 1 10))]
1713+
[n/remove-at-idx [5] [0 1 2 3 4 6 7 8 9]]
1714+
[n/remove-at-idx [9] (vec (range 9))]
1715+
[n/insert-before-idx [0 -1] (vec (range -1 10))]
1716+
[n/insert-before-idx [7 -1] [0 1 2 3 4 5 6 -1 7 8 9]]
1717+
[n/insert-before-idx [10 10] (vec (range 11))]]]
1718+
(is (= exp (-> (apply f (cons (transient v) args))
1719+
persistent!)))))))

0 commit comments

Comments
 (0)