2012-09-18 18 views
5

Estoy tratando de obtener the "official" example of clojure concurrency más cerca de una versión Java con bloqueo manual. In this gist puse el código java y clojure y el volcado de subprocesos de un perfil VisualVm de todas las versiones. aquí está el código clojure y el momentoPosibles mejoras en el ejemplo de concurrencia "oficial" de clojure (usando bloqueos, átomos, stm)

(ns simple-example (:gen-class)) 
(set! *warn-on-reflection* true) 
;; original from: http://clojure.org/concurrent_programming 
(import '(java.util.concurrent Executors Future) 
SimpleLocking$Node) 

(defn test-concur [iter refs nthreads niters] 
    (let [pool (Executors/newFixedThreadPool nthreads) 
     tasks (map (fn [t] 
         (fn [] 
         (dotimes [n niters] 
          (iter refs t)))) 
        (range nthreads))] 
    (doseq [^Future future (.invokeAll pool tasks)] 
     (.get future)) 
    (.shutdown pool))) 

(defn test-stm [nitems nthreads niters] 
    (let [refs (vec (map ref (repeat nitems 0))) 
     iter #(dosync (doseq [r %] (alter r + 1 %2)))] 
    (test-concur iter refs nthreads niters) 
    (map deref refs))) 

(defn test-atom [nitems nthreads niters] 
    (let [refs (vec (map atom (repeat nitems 0))) 
     iter #(doseq [r %] (swap! r + 1 %2))] 
    (test-concur iter refs nthreads niters) 
    (map deref refs))) 

;; SimpleLocking$Node is the class with the synchronized method of java version 
(defn test-locking [nitems nthreads niters] 
    (let [refs (->> (repeatedly #(SimpleLocking$Node.)) 
        (take nitems) vec) 
     iter #(doseq [^SimpleLocking$Node n %] (.sum n (+ 1 %2)))] 
    (test-concur iter refs nthreads niters) 
    (map (fn [^SimpleLocking$Node n] (.read n)) refs))) 

(definterface INode 
    (read []) 
    (add [v])) 

(deftype Node [^{:unsynchronized-mutable true} value] 
    INode 
    (read [_] value) 
    (add [this v] (set! value (+ value v)))) 

(defn test-locking-native [nitems nthreads niters] 
    (let [refs (->> (repeatedly #(Node. 0)) 
      (take nitems) vec) 
    iter #(doseq [^Node n %] 
      (locking n (.add n (+ 1 %2))))] 
    (test-concur iter refs nthreads niters) 
    (map (fn [^Node n] (.read n)) refs))) 

(defn -main [& args] 
    (read-line) 
    (let [[type nitems nthreads niters] (map read-string args) 
    t #(apply + (time (% nitems nthreads niters)))] 
    (case type 
     'lock (println "Locking:" (t test-locking)) 
     'atom (println "Atom:" (t test-atom)) 
     'stm (println "STM:" (t test-stm)) 
     'lock-native (println "Native locking:" (t test-locking-native))))) 

Tiempo (en un "viejo" Core Duo de Intel):

Java version 
int nitems=100; 
int nthreads=10; 
final int niters=1000; 
Sum node values: 5500000 
Time: 31 

simple-example=> (-main "lock" "100" "10" "1000") 
"Elapsed time: 60.030324 msecs" 
Locking: 5500000 
nil 
simple-example=> (-main "atom" "100" "10" "1000") 
"Elapsed time: 202.309477 msecs" 
Atom: 5500000 
nil 
simple-example=> (-main "stm" "100" "10" "1000") 
"Elapsed time: 1830.568508 msecs" 
STM: 5500000 
nil 
simple-example=> (-main "lock-native" "100" "10" "1000") 
"Elapsed time: 159.730149 msecs" 
Native locking: 5500000 
nil 

NOTA: No quiero obtener una versión clojure tan rápido como java uno, o una versión stm tan rápido como clojure usando bloqueos uno. Sé que en general es difícil y con algunos problemas imposibles. Sé que el uso de átomos y stm es más composable, más fácil de usar y menos propenso a errores que el uso de bloqueos manuales. Esas versiones son solo los mejores referentes posibles en java y clojure para el problema (bueno, hice mi mejor esfuerzo). Mi objetivo es acercar las versiones atom y stm a las de bloqueo, o comprender por qué (quizás en este ejemplo concreto) no es posible acelerar esas versiones.

NOTA: Otra comparacion, esta vez con versiones Haskell usando STM y MVARs (código en el mismo meollo vinculados):

>SimpleExampleMVar 100000 1000 6 
Starting... 
2100000000 
Computation time: 11.781 sec 
Done. 

>SimpleExampleSTM 100000 1000 6 
Starting... 
2100000000 
Computation time: 53.797 sec 
Done. 

>java -cp classes SimpleLocking 
Sum node values: 2100000000 
Time: 15.703 sec 

java -cp classes;%CLOJURE_JAR% simple_example lock 1000 6 100000 
"Elapsed time: 27.545 secs" 
Locking: 2100000000 

java -cp classes;%CLOJURE_JAR% simple_example lock-native 1000 6 100000 
"Elapsed time: 80.913 secs" 
Native locking: 2100000000 

java -cp classes;%CLOJURE_JAR% simple_example atom 1000 6 100000 
"Elapsed time: 95.143 secs" 
Atom: 2100000000 

java -cp classes;%CLOJURE_JAR% simple_example stm 1000 6 100000 
"Elapsed time: 990.255 secs" 
STM: 2100000000 
+1

de Clojure son herramientas para comprometer el rendimiento con el fin de lograr buenas abstracciones que tienen una semántica consistente en todas partes. El bloqueo manual que se implementa correctamente será más rápido casi todo el tiempo, pero generalmente es más difícil obtener la semántica de bloqueo correcta que utilizar Átomos/Refs/Agentes correctamente. – animal

+0

La pregunta está relacionada de alguna manera con esta (una versión de haskell): http://stackoverflow.com/questions/12475363/speed-up-haskell-concurrency – jneira

+1

@animal Soy consciente de esa compensación, pero la diferencia entre stm y átomos o bloqueo es demasiado grande. Tal vez el problema no es adecuado para el STM optimista? – jneira

Respuesta

1

usted no está realmente comparando con su semejante aquí - las versiones de Clojure son la creación e intercambio en nuevos números de recuadro inmutables, mientras que la versión de Java simplemente está topando con un contador primitivo mutable int en un método sincronizado.

Puede hacerlo al estilo de Java normal de bloqueo manual en Clojure con algo como:

(locking obj (set! (. obj fieldName) (+ 1 (.fieldName obj))))) 

La construcción locking es efectivamente equivalente a un bloque de código Java synchronized.

Si hace esto con un objeto Java de tipo insinuado o con un deftipo Clojure con un campo :unsynchronized-mutable, entonces creo que debería poder hacer coincidir el rendimiento puro de Java sincronizado.

no he probado esto, pero creo que debe trabajar con las primitivas, así (que podría ser útil si se está incrementando long contadores etc.) canónicas construcciones de programación concurrentes

+0

No estoy seguro de que el problema principal esté relacionado con los números del boxeo (aunque agrega algunos gastos generales). Al revisar los perfiles de la CPU se puede ver que la versión atom hace que 2007778 agregue llamadas, stm verion 2852124 y la versión de bloqueo (tanto java como clojure) 1000000 llamadas fijas. Además, la versión stm pasa más tiempo bloqueando hilos que otras. Tal vez este problema concreto (¡oficial!) Provoque demasiadas colisiones y reintentos. – jneira

Cuestiones relacionadas