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

编辑

尝试找到问题的根本原因——是关于构建性能,还是特定于数组到持久向量的操作?对于地图,我假设它是从数组到持久地图的操作。

已经存在一个从数组到向量的接口 (into [] arr),因此这也许真的是关于使其更快的问题(这实际上与我现在正在做的工作有关)。对于映射,最好的对应可能是 (apply hash-map arr),但这样就会选择一个实现,正如你所说的。

by
这主要是关于加快速度。我在互动 editorial.json 并从 @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 位服务器 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))
```

所以对于较小的尺寸,将数组列表移出循环似乎很重要?
by
edited 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编译器生成更快的代码(这似乎只对较小的集合尺寸很重要?)
by
希望最终,在向列表中添加一个常数而不是 `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))
评估次数:在 3 次调用的 6 个样本中,共有 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))
```
by
对于我的各种用例,从对象数组数据中快速构建路径会很有帮助。对于持久化向量,创建Owning是完美的,尽管我认为它不是最优的。

如果您提前有对象数组,应该有非常高效的创建向量或映射的方法 - 比创建临时的并遍历它们更有效。例如,在映射的情况下,如果你直接创建所有键的哈希码,你应该可以根据哈希码对它们进行排序,然后直接创建 HAMT 树,而无需太多的迭代循环,只需通过取哈希码索引数组的子集即可。

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

如果上述实现得很好,那么我可以在 JSON 解析案例中让持久数据结构优于或等于可变数据结构,因为我必须逐个迭代散列表的情况,而不是使用批量创建方法。
...