我发现持久化集合之间的结构相等性做出很少的假设,这导致了低效的实现,尤其是在向量和映射中。
实现的要点是通过方法进行分发,这些方法直接遍历底层数组。
这些实现可能不是最漂亮或最符合惯例的,但它们是高效的。如果这个功能在 Java 中实现,它的外观会有所不同。
我尝试了这些替代实现,并发现了显著的速度提升
向量
(let [die (clojure.lang.Reduced. false)]
(defn vec-eq
[^PersistentVector v ^Iterable y]
(let [iy (.iterator y)]
(.reduce v (fn [_ x] (if (= x (.next iy)) true die)) true))))
当比较向量和向量与列表时,这会很好地工作。
当前实现遍历从 0 到 count 的循环,并对每个元素调用 nth。nth 每次都调用 arrayFor(),而 reduce 和迭代器每个数组只获取一次底层数组。
映射
(let [o (Object.)
die (clojure.lang.Reduced. false)
eq (fn [m2] (fn [b k v]
(let [v' (.valAt ^IPersistentMap m2 k o)]
(if (.equals o v')
die
(if (= v v') true die)))))]
(defn map-eq
[m1 m2]
(.kvreduce ^IKVReduce m1 (eq m2) true)))
在这里,实现也直接遍历底层数组结构。
当前实现将数组转换为 seq,然后遍历它,并从其他映射通过 Map 接口获取条目。
此实现避免了将映射转换为序列,并且不会分配条目。
序列
当接收者是列表时,与之比较的对象和接收者将被转换为 seq。
可能通过迭代器将其他集合与其比较会更有效率。
(defn iter-eq
[^Iterable x ^Iterable y]
(let [ix (.iterator x)
iy (.iterator y)]
(loop []
(if (.hasNext ix)
(if (= (.next ix) (.next iy))
(recur)
false)
true))))
基准测试
使用 criterium,vec-eq 在两种情况下都获胜。随着大小的增加,回报逐渐减少,但即使在 n=64 时,vec-eq 的速度也比 = 快两倍。
对于较大的映射,map-eq 也是 2-3 倍快,对于较小的映射,快 10 倍。
(doseq [n [1 2 4 8 16 32 64]
:let [v1 (vec (range n))
v2 (vec (range n))]]
(println 'iter-eq n (iter-eq v1 v2))
(cc/quick-bench (iter-eq v1 v2))
(println 'vec-eq n (vec-eq v1 v2))
(cc/quick-bench (vec-eq v1 v2))
(println '= n (= v1 v2))
(cc/quick-bench (= v1 v2)))
(doseq [n [1 2 4 8 16 32 64]
:let [v1 (vec (range n))
v2 (list* (range n))]]
(println 'iter-eq n (iter-eq v1 v2))
(cc/quick-bench (iter-eq v1 v2))
(println 'vec-eq n (vec-eq v1 v2))
(cc/quick-bench (vec-eq v1 v2))
(println '= n (= v1 v2))
(cc/quick-bench (= v1 v2)))
(doseq [n [1 2 4 8 16 32 64]
:let [m1 (zipmap (range n) (range n))
m2 (zipmap (range n) (range n))]]
(cc/quick-bench (map-eq m1 m2))
(cc/quick-bench (= m1 m2)))
附加说明
还检查了以下情况
(doseq [n [10000 100000]
:let [v1 (vec (range n))
v2 (assoc v1 (dec (count v1)) 7)]]
(cc/quick-bench (vec-eq v1 v2))
(cc/quick-bench (iter-eq v1 v2))
(cc/quick-bench (= v1 v2)))
(doseq [n [100000]
:let [m1 (zipmap (range n) (range n))
m2 (assoc m1 (key (last m1)) 7)]]
(cc/quick-bench (map-eq m1 m2))
(cc/quick-bench (= m1 m2)))
优化实现仍然以巨大的优势获胜