2024 年 Clojure 状态调查 中分享您的想法!

欢迎!请参阅 关于 页面以获取有关如何操作的更多信息。

0
Clojure

当在应用程序中寻找执行速度时,在构建映射和向量时使用 java.util.ArrayList 是很有诱惑力的。然而,这样做也有一些陷阱。例如,对于向量,应该使用 LazilyPersistentVector/createOwning 而不是 PersistentVector/adopt,前者可以正确工作,而后者只适用于小于 32 的向量。

同样对于映射,如果映射中有少于 9 个条目,可以选择创建 PersistentArrayMap,但如果条目数为 9 个或更多,则应使用 PersistentHashMap

我不希望在这里提供解决方案,但 clojure.core 中有几个函数接受数组作为参数,并返回适当的数据结构
(array->vector arr) ;; 与 LazilyPersistentVector/createOwning 操作相同 (array->map arr) ;; 根据大小返回 PAM 或 PHM

1 个答案

0

编辑

试图找到根本问题所在——这是关于建设性能问题,还是特别关于数组转换成持久向量?对于映射,我认为它是kv数组转换成持久映射?

已经有了数组转换成向量的接口 (into [] arr),所以或许这其实是关于使其更快速(这实际上与我目前正在做的工作有关)。对于映射,最好的等效方式可能是 (apply hash-map arr),但正如您所说,它会选择一个实现。

by
主要是关于使事物更快。在尝试与data.json.playing around,并从@cnuernber最近的dtype-next工作受到一些启发,我在考虑使用java.util.ArrayList而不是瞬态clojure数据结构。

在存储映射中的东西时,我以[k1, v1, k2, v2...]的格式存储数据
by
比较ArrayList与PV以及瞬态,我看不到太大的区别

% java -version
openjdk版本"17.0.1" 2021-10-19
OpenJDK运行时环境Temurin-17.0.1+12 (构建17.0.1+12)
OpenJDK 64-Bit服务器VM Temurin-17.0.1+12 (构建17.0.1+12,混合模式,共享)
% clj -Sdeps '{:deps {org.clojure/clojure {:mvn/version "1.11.1"}}}'
Clojure 1.11.1

(defn make-al [^long c]
  (loop [i 0
         out (java.util.ArrayList.)]
    (if (< i c)
      (do
        (.add ^java.util.ArrayList out i)
        (recur (inc i) out))
      out))

(dotimes [_ 20] (time (make-al 10000000)))
   
;; drop 10
"Elapsed time: 118.936307 msecs"
"Elapsed time: 378.941495 msecs"
"Elapsed time: 451.090741 msecs"
"Elapsed time: 231.632739 msecs"
"Elapsed time: 533.601017 msecs"
"Elapsed time: 178.208623 msecs"
"Elapsed time: 566.648574 msecs"
"Elapsed time: 223.109572 msecs"
"Elapsed time: 404.011696 msecs"
"Elapsed time: 374.543087 msecs"

(defn make-pv [^long c]
  (loop [i 0
         out (transient [])
    (if (< i c)
          (recur (inc i) (conj! out i)))
          (persistent! out))))

(dotimes [_ 20] (time (make-pv 10000000)))

;; drop 10
"Elapsed time: 238.714966 msecs"
"Elapsed time: 139.776764 msecs"
"Elapsed time: 287.93457 msecs"
"Elapsed time: 212.816761 msecs"
"Elapsed time: 337.137074 msecs"
"Elapsed time: 386.315848 msecs"
"Elapsed time: 208.427246 msecs"
"Elapsed time: 291.750425 msecs"
"Elapsed time: 321.466389 msecs"
"Elapsed time: 251.875222 msecs"

但是,比较HashMap与PersistentMap以及瞬态,我确实看到了显著的差异(慢10倍)

(defn make-hm [^long c]
  (loop [i 0
         out (java.util.HashMap.)
    (if (< i c)
      (do
        (.put ^java.util.HashMap out i i)
        (recur (inc i) out))
      out))

(dotimes [_ 20] (time (make-hm 10000000)))

;; drop 10
"已过时间:949.351029 毫秒"
"已过时间:719.626631 毫秒"
"已过时间:664.274403 毫秒"
"已过时间:718.46743 毫秒"
"已过时间:880.380957 毫秒"
"已过时间:735.682114 毫秒"
"已过时间:611.627886 毫秒"
"已过时间:775.128433 毫秒"
"已过时间:537.130159 毫秒"
"已过时间:775.980626 毫秒"


(defn make-pm [^long c]
  (loop [i 0
         out (transient {})]
    (if (< i c)
      (recur (inc i) (assoc! out i i))
          (persistent! out))))

(dotimes [_ 20] (time (make-pm 10000000)))

"已过时间:5129.976085 毫秒"
"已过时间:5626.573669 毫秒"
"已过时间:5337.985317 毫秒"
"已过时间:5151.215195 毫秒"
"已过时间:5175.246837 毫秒"
"已过时间:5636.8717 毫秒"
"已过时间:5136.285851 毫秒"
"已过时间:5270.226782 毫秒"
"已过时间:5018.800694 毫秒"
"已过时间:5394.596752 毫秒"

基于此,我认为考虑如何使后者更快是一件很有趣的事情。
by
列表示例似乎更有趣一些。
```
clojure.data.json-perf-test>(quick-bench (make-pv 20))
评估次数:2706714 次,在 6 个 451119 次调用的样本中。
             执行时间平均值:218.595632 纳秒
    执行时间标准差:1.125857 纳秒
   执行时间下四分位数:216.403707 纳秒 ( 2.5%)
   执行时间上四分位数:219.447999 纳秒 (97.5%)
             开销使用:2.124369 纳秒

在 6 个样本中发现 1 个异常值(16.6667 %)
    低严重程度     1 (16.6667 %)
 异常值的方差:13.8889 %,方差因异常值而适度膨胀
;; => nil
clojure.data.json-perf-test>(quick-bench (make-al 20))
评估次数:5192388 次,在 6 个 865398 次调用的样本中。
             执行时间平均值:116.527862 纳秒
    执行时间标准差:5.242512 纳秒
   执行时间下四分位数:113.078599 纳秒 ( 2.5%)
   执行时间上四分位数:125.437194 纳秒 (97.5%)
             开销使用:2.124369 纳秒

在 6 个样本中发现 1 个异常值(16.6667 %)
    低严重程度     1 (16.6667 %)
 异常值的方差:13.8889 %,方差因异常值而适度膨胀
;; => nil
clojure.data.json-perf-test>(quick-bench (make-al-2 20))
评估次数:8299938 次,在 6 个 1383323 次调用的样本中。
             执行时间平均值:73.889678 纳秒
    执行时间标准差:4.373601 纳秒
   执行时间下四分位数:70.739550 纳秒 ( 2.5%)
   执行时间上四分位数:80.056859 纳秒 (97.5%)
             开销使用:2.124369 纳秒
;; => nil
clojure.data.json-perf-test>(quick-bench (make-pv 10000000))
评估次数:6 次,在 6 个 1 次调用的样本中。
             执行时间平均值:167.649450 毫秒
    执行时间标准差:50.505810 毫秒
   执行时间下四分位数:105.608206 毫秒 ( 2.5%)
   执行时间上四分位数:228.341248 毫秒 (97.5%)
             开销使用:2.124369 纳秒
;; => nil
clojure.data.json-perf-test>(quick-bench (make-al 10000000))
评估次数:6 次,在 6 个 1 次调用的样本中。
             执行时间平均值:149.405623 毫秒
    执行时间标准差:146.660162 毫秒
   执行时间下四分位数:64.432956 毫秒 ( 2.5%)
   执行时间上四分位数:332.819503 毫秒 (97.5%)
             开销使用:2.124369 纳秒
;; => nil
clojure.data.json-perf-test> (quick-bench (make-al-2 10000000))
评估次数:12次,在6个样本中每个样本2次调用。
             执行时间平均值:145.689554 毫秒
    执行时间标准偏差:71.749785 毫秒
   执行时间下四分位数:46.855186 毫秒 (2.5%)
   执行时间上四分位数:223.408978 毫秒 (97.5%)
             开销使用:2.124369 纳秒
;; => nil

```
将 `make-al-2` 定义为
```
(defn make-al-2 [^long c]
  (let [out (java.util.ArrayList.)]
    (loop [i 0]
      (when (< i c)
        (.add ^java.util.ArrayList out i)
        (recur (inc i))))
    out))
```

所以对于较小的尺寸,将 ArrayList 从循环中移出似乎很重要?
by
编辑 by
反编译 `make-al-2`
```
    public static Object invokeStatic(final long c) {
        final Object out = new ArrayList();
        for (long i = 0L; i < c; i = Numbers.inc(i)) {
            final Boolean b = ((ArrayList)out).add(Numbers.num(i)) ? Boolean.TRUE : Boolean.FALSE;
       }
        return out;
    }
```
反编译 `make-al`
```
    public static Object invokeStatic(final long c) {
        long i = 0L;
        Object out = new ArrayList();
        while (i < c) {
            final Boolean b = ((ArrayList)out).add(Numbers.num(i)) ? Boolean.TRUE : Boolean.FALSE;
            final long inc = Numbers.inc(i);
            out = out;
            i = inc;
       }
        return out;
    }
```
by
我猜这与持久对象是否比 ArrayList 更快或更慢无关,但似乎使用 ArrayList 让 Clojure 编译器生成更快的代码(这看起来只对较小的集合大小很重要?)
最后,将一个常量添加到列表中而不是 `i` 时,事情变化得更多。
```
clojure.data.json-perf-test>(quick-bench (make-pv 10000000))
评估次数:6 次,在 6 个 1 次调用的样本中。
             平均执行时间:112.911790 毫秒
    执行时间标准偏差:16.622923 毫秒
   执行时间下四分位数:100.571915 毫秒(2.5%)
   执行时间上四分位数:132.719706 毫秒(97.5%)
             开销使用:2.124369 纳秒
;; => nil
clojure.data.json-perf-test> (quick-bench (make-al-2 10000000))
评估次数:在 6 个样本中的 3 次调用,共计 18 次。
             平均执行时间:48.052989 毫秒
    执行时间标准偏差:7.951804 毫秒
   执行时间下四分位数:39.031470 毫秒(2.5%)
   执行时间上四分位数:58.394364 毫秒(97.5%)
             开销使用:2.124369 纳秒
;; => nil
clojure.data.json-perf-test>;
```

```
(defn make-pv [^long c]
   (let [])
   (loop [i 0
          out (transient [])]
     (if (< i c)
       (recur (inc i) (conj! out "3"))
       (persistent! out))))
```
```
 (defn make-al-2 [^long c]
   (let [out (java.util.ArrayList.)]
     (loop [i 0]
       (when (< i c)
         (.add ^java.util.ArrayList out "3")
         (recur (inc i))))
     out))
```
对于我的各种用例,从对象 [] 数据中快速构建是很有帮助的。虽然我认为创建 Owning 不是最优的,但对于持久向量来说,createOwning 是完美的。

如果你一开始就有对象数组,应该有创建向量或映射的非常高效的方法——比创建 transient 并逐个遍历它们更高效。例如,在映射的情况下,如果你一开始就创建所有键的哈希码,你应该能够根据哈希码重新排序它们,然后只需通过取哈希码索引数组的子部分,就可以直接创建 HAMT 树,而无需多次迭代循环。

对于持久向量的情况,这应该可以归结为直接从输入数组中创建长度为 32 的子部分,而不需要元素到元素的迭代。此路径允许客户端快速从其他来源构建这些数据结构,而不涉及手动迭代循环。

如果实现得当,我可以通过这种方式使持久数据结构在 JSON 解析案例中优于或匹配可变数据结构,因为我必须逐个迭代散列表中的项目,而不是使用批量创建方法。
...