Cómo mejorar el rendimiento de ClojureScript

Empecé a usar ClojureScript recientemente. Cuando reescribí un programa de JavaScript en ClojureScript, me preocupé por el rendimiento de ClojureScript.

Código ClojureScript

(def NUM 10000)
(def data
  (vec (repeatedly NUM #(hash-map :x (rand) :y (rand)))))

(.time js/console "cljs")
(loop [x 0 y 0 d data]
  (if (empty? d)
    [x y]
    (recur (+ x (:x (first d)))
           (+ y (:y (first d)))
           (rest d))))
(.timeEnd js/console "cljs")

Código JavaScript compilado (optimizaciones: espacios en blanco)

benchmark_cljs.benchmark.NUM = 1E4;
benchmark_cljs.benchmark.data = cljs.core.vec.call(null, cljs.core.repeatedly.call(null, benchmark_cljs.benchmark.NUM, function() {
  return cljs.core.PersistentHashMap.fromArrays.call(null, [new cljs.core.Keyword(null, "x", "x", 1013904362), new cljs.core.Keyword(null    , "y", "y", 1013904363)], [cljs.core.rand.call(null), cljs.core.rand.call(null)]);
}));
console.time("cljs");
var x_4753 = 0;
var y_4754 = 0;
var d_4755 = benchmark_cljs.benchmark.data;
while (true) {
  if (cljs.core.empty_QMARK_.call(null, d_4755)) {
    new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [x_4753, y_4754], null);
  } else {
    var G__4756 = x_4753 + (new cljs.core.Keyword(null, "x", "x", 1013904362)).cljs$core$IFn$_invoke$arity$1(cljs.core.first.call(null, d    _4755));
    var G__4757 = y_4754 + (new cljs.core.Keyword(null, "y", "y", 1013904363)).cljs$core$IFn$_invoke$arity$1(cljs.core.first.call(null, d    _4755));
    var G__4758 = cljs.core.rest.call(null, d_4755);
    x_4753 = G__4756;
    y_4754 = G__4757;
    d_4755 = G__4758;
    continue;
  }
  break;
}
console.timeEnd("cljs");

Código JavaScript

var NUM = 10000;
var data = [];
for (var i = 0; i < NUM; i++) {
  data[i] = {
    x: Math.random(),
    y: Math.random()
  }
}
console.time('js');
var x = 0;
var y = 0;
for (var i = 0; i < data.length; i++) {
  x += data[i].x;
  y += data[i].y;
}
console.timeEnd('js');

El código ClojureScript y el código JavaScrpt están haciendo las mismas cosas, pero cada tiempo de proceso es diferente.

Tiempo de procesamiento

ClojureScript(optimizations :whitespace): 30 〜 70ms
ClojureScript(optimizations :advanced): 9 〜 13ms
JavaScript: 0.3ms 〜 0.9ms

Dígame cómo mejorar el tiempo de procesamiento de ClojureScript.

Gracias de antemano.

preguntado el 12 de febrero de 14 a las 07:02

¿Puede publicar la salida de JavaScript compilada generada por ClojureScript? -

Agregué código JavaScript compilado. -

Dado que las estructuras de datos nativas de Clojure son inmutables, es incorrecto decir que JS y ClJS están haciendo lo mismo. Estos son dos lenguajes diferentes, incluso si ClJS está compilado en JS. -

2 Respuestas

Está utilizando estructuras de datos persistentes en ClojureScript y matrices y objetos mutables en JavaScript. Es de esperar que las características de rendimiento de los dos fragmentos sean diferentes.

Ahora, si el rendimiento es realmente crítico para lo que estás haciendo y la persistencia no proporciona ningún beneficio, solo puede usar matrices y objetos de ClojureScript:

(def NUM 10000)
(def data (array))
(loop [i 0]
  (when (< i NUM)
    (aset data i (js-obj "x" (js/Math.random) "y" (js/Math.random)))
    (recur (inc i))))

(let [lim (alength data)]
  (loop [x 0 y 0 i 0]
    (if (< i lim)
      (recur (+ x (aget data i "x"))
             (+ y (aget data i "y"))
             (inc i))
      (println x y))))

Por otro lado, si necesita conservar versiones antiguas de las estructuras de datos involucradas, probablemente recuperará el "tiempo perdido" al no tener que hacer copias completas para preservarlas.

Respondido 12 Feb 14, 07:02

Estaba pensando que tal vez un for la comprensión se compilaría muy bien en JavaScript, algo así como: (def data (vec (for [x (range NUM)] {:x (rand) :y (rand)}))) - elclanrs

Claro, funcionaría bien. El resultado final seguiría siendo un vector persistente de mapas de matrices persistentes, con todas las implicaciones de rendimiento. (Bueno, en realidad, el OP usa mapas hash muy pequeños, por lo que este enfoque probablemente funcionaría un poco mejor). Michał Marczyk

@elclanrs for las comprensiones generan secuencias perezosas. Si bien es agradable, no verá el tipo de rendimiento demostrado por esta respuesta. - dnolen

¡Gracias, @Michał! Podría mejorar el tiempo de proceso usando "matriz" para mi programa. - rapé

Tiene una serie de opciones aquí dependiendo de cuánto rendimiento necesita y de lo que está dispuesto a renunciar. pongo un poco los puntos de referencia arriba en GitHub si estás interesado.

Mediante el uso de registros y acceso a campos nativos, puede reducir a la mitad el tiempo de ejecución de su solución ClojureScript original:

(defrecord XY [x y])
(def data (mapv (fn [_] (XY. (rand) (rand))) (range NUM))) 

(defn sumXsAndYsWithLoopAndNativeFieldAccess [data]
  (loop [x 0 y 0 data data]
    (if (seq data)
      (let [o (first data)]
        (recur (+ x (.-x o)) (+ y (.-y o)) (rest data)))
      [x y])))

(time (sumXsAndYsWithLoopAndNativeFieldAccess data))

También puede usar matrices como locales mutables y obtener una solución solo 8 veces más lenta que la versión nativa de JavaScript:

(defn sumsXsAndYsWithDotimesNativeFieldAccessAndMutableLocals [data]
  (let [x (doto (make-array 1)
            (aset 0 0))
        y (doto (make-array 1)
            (aset 0 0))]
    (dotimes [i (count data)]
      (let [o (data i)]
        (aset x 0 (+ (aget x 0) (.-x o)))
        (aset y 0 (+ (aget y 0) (.-y o)))))
    [(aget x 0) (aget y 0)]))

(time (sumsXsAndYsWithDotimesNativeFieldAccessAndMutableLocals data))

Además, puede usar lo anterior junto con arreglos y lograr una solución aproximadamente 3 veces más lenta que la versión nativa de JavaScript:

(def data (into-array (mapv #(XY. (rand) (rand)) (range NUM))))

(defn sumsXsAndYsWithDotimesOnArrayNativeFieldAccessAndMutableLocals [data]
  (let [x (doto (make-array 1)
            (aset 0 0))
        y (doto (make-array 1)
            (aset 0 0))]
    (dotimes [i (alength data)]
      (let [o (aget data i)]
        (aset x 0 (+ (aget x 0) (.-x o)))
        (aset y 0 (+ (aget y 0) (.-y o)))))
    [(aget x 0) (aget y 0)]))

(time (sumsXsAndYsWithDotimesOnArrayNativeFieldAccessAndMutableLocals data))

Es posible que desee ver la de David Nolen recámara proyecto. Tiene algunas macros agradables para crear y actualizar mutables locales que hacen que lo anterior no parezca ridículo.

De todos modos, espero que eso ayude.

Respondido 18 Feb 14, 06:02

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.