【问题标题】:Clojure: why is aget so slow?Clojure:为什么 get 这么慢?
【发布时间】:2023-03-20 11:36:01
【问题描述】:

在我看来,与 java 数组相比,clojure 向量的性能略有下降。因此,我认为“传统智慧”是对于代码中对性能至关重要的部分,最好使用 java 数组。

然而,我的测试表明这不是真的:

Clojure 1.3.0
user=> (def x (vec (range 100000)))
#'user/x
user=> (def xa (int-array x))
#'user/xa
user=> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (nth x i))) s)))
"Elapsed time: 16.551 msecs"
4999950000
user=> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (aget xa i))) s)))
"Elapsed time: 1271.804 msecs"
4999950000

如您所见,get 增加了大约 800% 的时间。不过,这两种方法仍然比原生 java 慢:

public class Test {                                                                                                                                                                                                                                                                                                           
    public static void main (String[] args) {                                                                                                                                                                                                                                                                                 
        int[] x = new int[100000];                                                                                                                                                                                                                                                                                            
        for (int i=0;i<100000;i++) {                                                                                                                                                                                                                                                                                          
            x[i]=i;                                                                                                                                                                                                                                                                                                           
        }                                                                                                                                                                                                                                                                                                                     
        long s=0;                                                                                                                                                                                                                                                                                                             
        long end, start = System.nanoTime();                                                                                                                                                                                                                                                                                  
        for (int i=0;i<100000;i++) {                                                                                                                                                                                                                                                                                          
            s+= x[i];                                                                                                                                                                                                                                                                                                         
        }                                                                                                                                                                                                                                                                                                                     
        end = System.nanoTime();                                                                                                                                                                                                                                                                                              
        System.out.println((end-start)/1000000.0+" ms");                                                                                                                                                                                                                                                                      
        System.out.println(s);                                                                                                                                                                                                                                                                                                
    }                                                                                                                                                                                                                                                                                                                         
}                              

> java Test
1.884 ms
4999950000

那么,我的结论应该是 get 比 nth 慢 80 倍,比 java 中的 []-access 慢大约 800 倍吗?

【问题讨论】:

标签: arrays performance clojure


【解决方案1】:

我怀疑这是由于aget函数对原始类型的反射和自动装箱......

幸运的是,aget/aset 为原始数组提供了高性能的重载,可以避免反射,只需执行 array[i] 直接访问(请参阅 herehere)。

你只需要传递一个类型提示来选择正确的函数。

(type xa)
[I    ; indicates array of primitive ints

; with type hint on array
;
(time (loop [i 0 s 0] 
        (if (< i 100000) (recur (inc i) 
          (+ s (aget ^ints xa i))) s))) 
"Elapsed time: 6.79 msecs"
4999950000

; without type hinting
;
(time (loop [i 0 s 0] 
        (if (< i 100000) (recur (inc i) 
          (+ s (aget xa i))) s)))
"Elapsed time: 1135.097 msecs"
4999950000

【讨论】:

  • 两种情况都被自动装箱
  • 谢谢!通过类型提示,我的 java 数组解决方案现在是 clojure 向量解决方案的两倍。尽管如此,它的速度还是原生 java 的 5 倍,所以我想“真正的”解决方案是将这部分代码分解为一个普通的 java 类,对吧?
  • @Claude - 这是比较两种方法的相对性能的有趣练习,因此有点人为。但是,您是否曾质疑“对 100,000 个整数序列求和的最佳/最快方法是什么?”那么 NielsK 提出的一个简单的(reduce + xa) 显然是更好的(也是最惯用的)方法。我假设你需要做更多的事情,这就是让你走上这条路的原因。
【解决方案2】:

看起来反射正在侵蚀你所有测试的准确性:

user> (set! *warn-on-reflection* true)
true
user> (def x (vec (range 100000)))
#'user/x
user>  (def xa (int-array x))
#'user/xa
user>  (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (nth x i))) s)))
NO_SOURCE_FILE:1 recur arg for primitive local: s is not matching primitive, had: Object, needed: long
Auto-boxing loop arg: s
"Elapsed time: 12.11893 msecs"
4999950000
user> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (aget xa i))) s)))
Reflection warning, NO_SOURCE_FILE:1 - call to aget can't be resolved.
NO_SOURCE_FILE:1 recur arg for primitive local: s is not matching primitive, had: Object, needed: long
Auto-boxing loop arg: s
Reflection warning, NO_SOURCE_FILE:1 - call to aget can't be resolved.
"Elapsed time: 2689.865468 msecs"
4999950000
user> 

第二个恰好有更多的反思。

在运行这种基准测试时,请务必多次运行以使 hotSpot 编译器预热

user> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (aget xa i))) (long s))))
"Elapsed time: 3135.651399 msecs"
4999950000
user> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ (long s) (aget xa i))) (long s))))
"Elapsed time: 1014.218461 msecs"
4999950000
user> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ (long s) (aget xa i))) (long s))))
"Elapsed time: 998.280869 msecs"
4999950000
user> (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ (long s) (aget xa i))) (long s))))
"Elapsed time: 970.17736 msecs"
4999950000

在这种情况下,几次运行将其降低到原始时间的 1/3(尽管反射仍然是这里的主要问题)

如果我用 dotimes 热身,结果会改善很多:

(dotimes [_ 1000]  (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ s (nth x  i))) s))))
"Elapsed time: 3.704714 msecs"

(dotimes [_ 1000] (time  (loop [i 0 s 0] (if (< i 100000) (recur (inc i) (+ (long s) (aget xa i))) (long s)))))
"Elapsed time: 936.03987 msecs"

【讨论】:

  • github.com/hugoduncan/criterium 适合使用 JIT 预热等进行基准测试。
  • 实际上,这是对实际问题的最佳答案:为什么它很慢。仍然隐含的问题 - 如何加快速度 - 确实是通过在数组上使用类型提示。不过感谢您的澄清!
【解决方案3】:

似乎根本不需要类型提示,Clojure 开箱即用地优化。

当需要对集合执行多元函数时,只需使用 apply 和函数。当您需要将函数应用于集合中的元素并将结果存储在累加器中时,请使用 reduce。在这种情况下,两者都适用。

=> (def xa (into-array (range 100000)))
#'user/xa

=> (time (apply + xa))
"Elapsed time: 12.264753 msecs"
4999950000

=>(time (reduce + xa))
"Elapsed time: 2.735339 msecs"
4999950000

甚至更简单的方法也可以消除这些差异,尽管比上述最佳情况稍慢:

=> (def xa (range 100000))
#'user/xa

=> (time (apply + xa))
"Elapsed time: 4.547634 msecs"
4999950000

=> (time (reduce + xa))
"Elapsed time: 4.506572 msecs"

尽量编写最简单的代码,如果不够快,请优化。

【讨论】:

  • 在这种情况下你是对的,但显然我的真实示例比添加东西要复杂得多,并且需要来自不同数组的agets。实际上,您第一次测试中的差异与编译器优化有关,当您首先运行 reduce 然后运行 ​​apply 时,apply 更快。
  • 重复测试; apply 停留在 ca 12 ms,reduce 也从 ca 12s 开始,但稳定在 ca 2.7 ms。这就是为什么带有老化时间的测试在 JVM 上很重要。我赞成 Sw1nn 的回答,以防需要真正的随机访问。通常可以应用基于 Clojure 核心函数的函数式算法,利用其内部优化和惰性等优势。
猜你喜欢
  • 2015-06-09
  • 1970-01-01
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 2016-09-28
  • 2020-02-08
  • 2012-07-17
  • 2011-11-07
相关资源
最近更新 更多