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),但这样会选择一个实现。

这主要关于提高效率。在处理 data.json 并从 @cnuernberrecent 在 dtype-next 方面的最近工作时获得一些灵感,我正在检查是否可以使用 java.util.ArrayList 而不是暂态的 clojure 数据结构。

在存储映射中的数据时,我使用 [k1, v1, k2, v2...] 的格式存储数据。
比较 ArrayList 与携带暂态的 PV,我没有看到太多的差异。

% java -version
openjdk版本 "17.0.1" 2021-10-19
OpenJDK 运行时环境 Temurin-17.0.1+12 (构建 17.0.1+12)
OpenJDK 64 位服务器 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
"经过时间:118.936307 毫秒"
"经过时间:378.941495 毫秒"
"经过时间:451.090741 毫秒"
"经过时间:231.632739 毫秒"
"经过时间:533.601017 毫秒"
"经过时间:178.208623 毫秒"
"经过时间:566.648574 毫秒"
"经过时间:223.109572 毫秒"
"经过时间:404.011696 毫秒"
"经过时间:374.543087 毫秒"

(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
"经过时间:238.714966 毫秒"
"经过时间:139.776764 毫秒"
"经过时间:287.93457 毫秒"
"经过时间:212.816761 毫秒"
"经过时间:337.137074 毫秒"
"经过时间:386.315848 毫秒"
"经过时间:208.427246 毫秒"
"经过时间:291.750425 毫秒"
"经过时间:321.466389 毫秒"
"经过时间:251.875222 毫秒"

但是,比较 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))
```

因此,对于较小的尺寸,将 array-list 移出循环似乎很重要?

编辑
反编译 `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;
    }
```
我认为这与瞬态是否比 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))
评估数量:18次,来源于3个调用的6个样本。
             执行时间平均值: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))
```
对于我各种用例,从 object[] 数据快速构建路径会非常有帮助。虽然我认为 createOwning 对于持久向量来说已经很完美了,但并不是最优。

如果你事先有对象数组,应该有非常高效的创建向量和映射的方法 - 比创建瞬态对象循环遍历更高效。例如,在映射的情况下,如果你立即创建所有键的哈希码,你应该能够根据哈希码重新排序它们,然后通过直接从哈希码索引数组取子数组,简单地创建 HAMT 树,而不需要做太多迭代循环。

对于持久向量的情况,应该归纳为从输入数组中取长度为 32 的子数组,并直接从这些子数组创建 HAMT,而不进行元素逐个迭代。这条路径允许客户快速从其他来源不经手动迭代循环构建这些数据结构。

如果上述实现得当时,我可以在 json 解析案例中使持久数据结构超越或匹配可变数据结构,因为我在哈希表案例中必须逐个迭代项目,而不是使用批处理创建方法。
...