2012-04-12 74 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

相關問題