Fast vector math en Clojure/Incanter
(6)
Mis soluciones finales
Después de todas las pruebas, encontré dos formas ligeramente diferentes de hacer el cálculo con la velocidad suficiente.
Primero he usado la función diff
con diferentes tipos de valores de retorno, a continuación aparece el código que devuelve un vector, pero también he programado una versión que devuelve una matriz doble (replace (vec y) con y) y Incanter.matrix (replace) (vec y) con matriz y). Esta función solo se basa en matrices Java. Esto se basa en el código de Jouni con algunos consejos de tipo extra eliminados.
Otro enfoque es hacer los cálculos con matrices de Java y almacenar los valores en un vector transitorio. Como puede ver en los tiempos, esto es un poco más rápido que el acercamiento 1 si no quiere que la función regrese y arrastre. Esto se implementa en función difft
.
Entonces la elección realmente depende de lo que quieras hacer con los datos. Supongo que una buena opción sería sobrecargar la función para que devuelva el mismo tipo que se utilizó en la llamada. En realidad, pasar una matriz java a diff en lugar de un vector hace ~ 1s más rápido.
Tiempos para las diferentes funciones:
vector de retorno de diff:
(time (def y (diff x)))
"Elapsed time: 4733.259 msecs"
diff que devuelve Incanter.matrix:
(time (def y (diff x)))
"Elapsed time: 2599.728 msecs"
diff que devuelve doble matriz:
(time (def y (diff x)))
"Elapsed time: 1638.548 msecs"
difft:
(time (def y (difft x)))
"Elapsed time: 3683.237 msecs"
Las funciones
(use ''incanter.stats)
(def x (vec (sample-normal 1e7)))
(defn diff [x]
(let [y (double-array (dec (count x)))
x (double-array x)]
(dotimes [i (dec (count x))]
(aset y i
(- (aget x (inc i))
(aget x i))))
(vec y)))
(defn difft [x]
(let [y (vector (range n))
y (transient y)
x (double-array x)]
(dotimes [i (dec (count x))]
(assoc! y i
(- (aget x (inc i))
(aget x i))))
(persistent! y)))
Actualmente estoy buscando en Clojure y Incanter como alternativa a R. (No es que no me guste R, pero es interesante probar nuevos lenguajes.) Me gusta Incanter y encuentro la sintaxis atractiva, pero las operaciones vectorizadas son bastante lentas en comparación por ejemplo, a R o Python.
Como ejemplo, quería obtener la diferencia de primer orden de un vector usando las operaciones del vector de Incanter, el mapa Clojure y R. Debajo está el código y el tiempo para todas las versiones. Como pueden ver, R es claramente más rápido.
Incanter y Clojure:
(use ''(incanter core stats))
(def x (doall (sample-normal 1e7)))
(time (def y (doall (minus (rest x) (butlast x)))))
"Elapsed time: 16481.337 msecs"
(time (def y (doall (map - (rest x) (butlast x)))))
"Elapsed time: 16457.850 msecs"
R:
rdiff <- function(x){
n = length(x)
x[2:n] - x[1:(n-1)]}
x = rnorm(1e7)
system.time(rdiff(x))
user system elapsed
1.504 0.900 2.561
Entonces, me preguntaba si hay alguna forma de acelerar las operaciones vectoriales en Incanter / Clojure. También se aceptan soluciones que impliquen el uso de bucles, matrices de Java y / o bibliotecas de Clojure.
También he publicado esta pregunta en el grupo de Google Incanter sin respuestas hasta el momento.
ACTUALIZACIÓN: He marcado la respuesta de Jouni como aceptada, ver a continuación mi propia respuesta, donde he limpiado un poco su código y he añadido algunos puntos de referencia.
Aquí hay una implementación de arreglos de Java que está en mi sistema más rápido que su código R (YMMV). Tenga en cuenta que habilita las advertencias de reflexión, que es esencial para optimizar el rendimiento, y la sugerencia de tipo repetida en y (la que está en la definición no parece ayudar al aset) y convertir todo en valores dobles primitivos (los puntos débiles se aseguran de que i es un int primitivo).
(set! *warn-on-reflection* true)
(use ''incanter.stats)
(def ^"[D" x (double-array (sample-normal 1e7)))
(time
(do
(def ^"[D" y (double-array (dec (count x))))
(dotimes [i (dec (count x))]
(aset ^"[D" y
i
(double (- (double (aget x (inc i)))
(double (aget x i))))))))
Aquí hay una solución con transitorios: atractiva pero lenta.
(use ''incanter.stats)
(set! *warn-on-reflection* true)
(def x (doall (sample-normal 1e7)))
(time
(def y
(loop [xs x
xs+ (rest x)
result (transient [])]
(if (empty? xs+)
(persistent! result)
(recur (rest xs) (rest xs+)
(conj! result (- (double (first xs+))
(double (first xs)))))))))
No es específico para su código de ejemplo, pero dado que esto se convirtió en una discusión sobre el rendimiento de Clojure, puede disfrutar este enlace: Clojure es rápido
Todos los comentarios hasta ahora son de personas que no parecen tener mucha experiencia acelerando el código de Clojure. Si desea que el código de Clojure funcione de manera idéntica a Java, las instalaciones están disponibles para hacerlo. Sin embargo, puede tener más sentido pasar a bibliotecas maduras de Java como Colt o Parallel Colt para matemáticas vectoriales. Puede tener sentido utilizar matrices de Java para la iteración de rendimiento más alta absoluta.
El enlace de @Shane está tan lleno de información desactualizada que apenas vale la pena mirar. Además, el comentario de @ Shane de que el código es más lento que el factor 10 es simplemente inexacto (y no es compatible http://shootout.alioth.debian.org/u32q/compare.php?lang=clojure , y estos puntos de referencia no tienen en cuenta el tipos de optimización posibles en 1.2.0 o 1.3.0-alpha1). Con un poco de trabajo, generalmente es fácil obtener el código Clojure con 4X-5X. Más allá de eso, por lo general, se requiere un conocimiento más profundo de los caminos rápidos de Clojure; algo no se difunde ampliamente ya que Clojure es un lenguaje bastante joven.
Clojure es bastante rápido. Pero aprender a hacerlo rápido va a tomar un poco de trabajo / investigación ya que Clojure desalienta las operaciones mutables y las estructuras de datos mutables.
El blog de Bradford Cross tiene un montón de publicaciones sobre esto (usa esto para la startup que trabaja en el texto del enlace . En general, usar transitorios en bucles internos, tipo de alusión (a través de *warn-on-reflection*
) etc. son buenos para la velocidad aumenta. La Alegría de Clojure tiene una excelente sección de ajuste del rendimiento, que deberías leer.