2012-04-12 41 views
4

在我的思考中,clojure向量与java数组相比有轻微的性能提升。因此,我认为“传统智慧”是对于那些性能至关重要的代码部分,最好使用java数组。然而Clojure:为什么年龄如此之慢?

我的测试表明,这是不正确的:

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 

正如你所看到的,皮亚杰增加了约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 

所以,应我的结论是皮亚杰比第n个慢80倍,比[] - 访问在java中慢大约800倍?

+0

很多工作已经进入让你不必进行这种优化:) – 2012-04-13 00:07:58

+0

发表在奇怪的aget优化行为的后续行为http://stackoverflow.com/questions/10144937/strange-aget-optimisation-行为 – NielsK 2012-04-13 16:47:50

回答

8

我怀疑这是到反思和经销商原始类型由AGET功能的拳击....

幸运AGET/ASET具有该避免反射和只是做一个直接序列[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 
+0

两种情况都被自动装盒 – 2012-04-12 23:43:53

+0

谢谢!通过类型提示,我的java数组解决方案现在是clojure矢量的两倍。尽管如此,它比原生Java慢5倍,所以我猜“真正”的解决方案是将这部分代码分解成普通的Java类,对吧? – Claude 2012-04-13 14:11:57

+0

@Claude--这是一个有趣的练习,比较两种方法的相对表现,因此有点人为的。但是,如果您问题是“对100,000个整数的序列进行总结,最好/最快的方法是什么?”那么NielsK提出的一个简单的'(reduce + xa)'就是明显优越的(也是最习惯的)方法。我假设你需要做更多的事情,而这正是你走上这条路的原因。 – sw1nn 2012-04-13 14:32:14

4

它看起来像反射洗出所有测试的准确度:

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> 

第二个正好在它更多的思考。

当运行这种基准一定要运行它多次获得热点编译器在这种情况下,一些运行下降下来到1/3原来的时间热身

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 

(虽然反射仍是这里的主要问题)

如果我温暖他们两个了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" 
+2

https://github.com/hugoduncan/criterium适合与JIT热身等基准测试。 – sw1nn 2012-04-12 23:54:09

+0

其实,这是实际问题的最佳答案:为什么它很慢。仍然隐含的问题 - 如何加速 - 实际上是通过在数组上使用类型提示。谢谢你的澄清! – Claude 2012-04-13 14:13:43

4

似乎不需要任何类型提示,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" 

刚刚尝试写简单的代码可能,并且只有这不是足够快,优化。

+0

在这种情况下,你是对的,但显然我的现实生活中的例子比添加东西更复杂,而且需要来自不同阵列的'aget's。实际上,第一次测试中的差异与编译器优化有关,当您首次运行reduce和apply时,apply是更快的。 – Claude 2012-04-13 14:09:35

+2

测试重复完成;申请停留在约12毫秒,减少开始在约12秒,但稳定在约2.7毫秒。这就是为什么在JVM上使用老化时间进行测试非常重要。我提出了Sw1nn的回答,以防需要真正的随机访问。通常可以应用基于Clojure核心功能的函数式算法,利用其内部优化以及懒惰等优点。 – NielsK 2012-04-13 15:07:34