NodeJs è più veloce di Clojure?

Ho appena iniziato a studiare Clojure. Una delle prime cose che ho notato è che non ci sono loop. Va bene, posso ripeterti. Diamo un’occhiata a questa funzione (da Practical Clojure):

(defn add-up "Adds up numbers from 1 to n" ([n] (add-up n 0 0)) ([ni sum] (if (< ni) sum (recur n (+ 1 i) (+ i sum))))) 

Per ottenere la stessa funzione in Javascript, usiamo un ciclo come questo:

 function addup (n) { var sum = 0; for(var i = n; i > 0; i--) { sum += i; } return sum; } 

Al termine, i risultati sembrano:

 input size: 10,000,000 clojure: 818 ms nodejs: 160 ms input size: 55,000,000 clojure: 4051 ms nodejs: 754 ms input size: 100,000,000 clojure: 7390 ms nodejs: 1351 ms 

Ho quindi proceduto a provare il classico fib (dopo aver letto questo ):

in clojure:

 (defn fib "Fib" [n] (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))) 

in js:

 function fib (n) { if (n <= 1) return 1; return fib(n-1) + fib(n-2); } 

Ancora una volta, la performance ha una certa differenza.

 fib of 39 clojure: 9092 ms nodejs: 3484 ms fib of 40 clojure: 14728 ms nodejs: 5615 ms fib of 41 clojure: 23611 ms nodejs: 9079 ms 

Nota Sto usando (time (fib 40)) in clojure, quindi sta ignorando il tempo di avvio per JVM. Questi sono eseguiti su un MacBook Air (Intel Core 2 Duo 1.86 Ghz).

Quindi cosa sta causando Clojure a essere lento qui? E perché la gente dice che “Clojure è veloce”?

Grazie in anticipo e per favore, niente guerre di fiamma.

 (set! *unchecked-math* true) (defn add-up ^long [^long n] (loop [nni 0 sum 0] (if (< ni) sum (recur n (inc i) (+ i sum))))) (defn fib ^long [^long n] (if (<= n 1) 1 (+ (fib (dec n)) (fib (- n 2))))) (comment ;; ~130ms (dotimes [_ 10] (time (add-up 1e8))) ;; ~1180ms (dotimes [_ 10] (time (fib 41))) ) 

Tutti i numeri da 2,66ghz i7 Macbook Pro OS X 10.7 JDK 7 64 bit

Come puoi vedere, Node.js viene battuto. Questo è con alpha 1.3.0, ma puoi ottenere la stessa cosa in 1.2.0 se sai cosa stai facendo.

Sulla mia macchina Node.js 0.4.8 per addup 1e8 era ~ 990ms, per fib 41 ~ 7600ms.

  Node.js | Clojure | add-up 990ms | 130ms | fib(41) 7600ms | 1180ms 

In realtà, mi aspetto che Clojure sia significativamente più veloce di Javascript se ottimizzi il tuo codice per le prestazioni.

Clojure compilerà staticamente un bytecode Java ottimizzato abbastanza quando fornirai abbastanza informazioni di tipo statico (ad esempio, suggerimenti di tipo o cast ai tipi primitivi). Quindi, almeno in teoria, dovresti essere in grado di avvicinarti alla pura velocità di Java, che è a sua volta molto vicina alle prestazioni del codice nativo.

Quindi provalo!

In questo caso, esistono diversi problemi che causano il rallentamento del codice Clojure:

  • Clojure supporta l’aritmetica arbitraria di precisione per impostazione predefinita, quindi tutte le operazioni aritmetiche vengono automaticamente controllate per l’overflow e se i numeri necessari vengono promossi a BigIntegers ecc. Questo controllo extra aggiunge una piccola quantità di overhead che è generalmente trascurabile, ma che può essere visualizzata se si esegue l’aritmetica operazioni in un circuito chiuso come questo. Il modo più semplice per risolverlo in Clojure 1.2 è usare le funzioni deselezionate * (questo è un po ‘poco elegante, ma sarà molto migliorato in Clojure 1.3)
  • A meno che tu non lo dica diversamente, Clojure si comporta in modo dinamico e mette in discussione gli argomenti della funzione. Quindi sospetto che il tuo codice stia creando e inscatolando molti interi / lunghi. Il modo per rimuovere questo per le variabili del ciclo è utilizzare i suggerimenti di tipo primitivo e utilizzare costrutti come loop / recur.
  • Allo stesso modo, n è in box, il che significa che la funzione <= non può essere ottimizzata per usare l'aritmetica primitiva. Puoi evitarlo gettando n in una lunga primitiva con un let locale.
  • (time (some-function)) è anche un modo inaffidabile per eseguire il benchmark in Clojure perché non consente necessariamente l’ottimizzazione della compilazione JIT. Spesso è necessario eseguire (alcune-funzioni) alcune volte prima, in modo che il JIT abbia una possibilità di fare il suo lavoro.

Il mio suggerimento per la versione ottimizzata di Clojure di add-up sarebbe quindi qualcosa di più simile a:

 (defn add-up "Adds up numbers from 1 to n" [n] (let [n2 (long n)] ; unbox loop limit (loop [i (long 1) ; use "loop" for primitives acc (long 0)] ; cast to primitive (if (<= i n2) ; use unboxed loop limit (recur (unchecked-inc i) (unchecked-add acc i)) ; use unchecked maths acc)))) 

E un modo migliore per farlo è il seguente (per consentire la compilazione JIT):

 (defn f [] (add-up 10000000)) (do (dotimes [i 10] (f)) (time (f))) 

Se faccio quanto sopra, ottengo 6 ms per la soluzione Clojure in Clojure 1.2. Il che è qualcosa come 15-20 volte più veloce del codice Node.js e forse 80-100 volte più veloce della versione originale di Clojure.

Per inciso, questo è anche il più veloce che posso ottenere questo ciclo per andare in Java puro, quindi dubito che sarebbe ansible migliorare molto in qualsiasi linguaggio JVM. Ci mette anche a circa 2 cicli macchina per iterazione ... quindi probabilmente non è lontano dalla velocità del codice macchina nativo!

(mi dispiace non essere in grado di confrontarmi con Node.js sulla mia macchina, ma è un core i7 980X a 3,3 GHz per chiunque sia interessato)

Un commento di alto livello Node.js e Clojure hanno modelli completamente diversi per raggiungere la scalabilità e, infine, rendere il software veloce.

Clojure raggiunge la scalabilità attraverso il parallelismo multi-core. Se si compilano correttamente i programmi Clojure, è ansible suddividere il proprio lavoro computazionale (tramite pmap , ecc.) Per eseguirlo in parallelo su core separati.

Node.js non è parallelo. Piuttosto, la sua intuizione chiave è che la scalabilità (di solito in un ambiente di applicazioni Web) è legata all’I / O. Pertanto, la tecnologia Node.js e Google V8 raggiunge la scalabilità tramite molti callback I / O asincroni.

In teoria, mi aspetterei che Clojure sconfiggesse Node.js in aree facilmente parallelizzabili. Fibonacci rientrerebbe in questa categoria e avrebbe battuto Node.js se avesse dato abbastanza core. E Node.js sarebbe meglio per le applicazioni lato server che fanno molte richieste al file system o alla rete.

In conclusione, non penso che questo possa essere un ottimo punto di riferimento per il confronto tra Clojure e Node.js.

Un paio di suggerimenti, supponendo che tu stia usando il clojure 1.2

  • ripetere i test (time …) è probabile che aumenti le velocità in clojure, a causa dell’ottimizzazione del JIT.
  • (inc i) è – un po ‘più veloce di (+ i 1)
  • le funzioni deselezionate- * sono anche più veloci (a volte MOLTO più veloci) rispetto alle loro varianti selezionate. Supponendo che non sia necessario superare il limite di long o double, usare unchecked-add, unchecked-int ecc potrebbe essere molto più veloce.
  • leggere su dichiarazioni di tipo; in alcuni casi, possono anche migliorare notevolmente la velocità.

Clojure 1.3 è generalmente più veloce con numeri che 1.2, ma è ancora in fase di sviluppo.

Quanto segue è circa 20 volte più veloce della tua versione, e può ancora essere migliorato modificando l’algoritmo (eseguendo il conto alla rovescia, come fa la versione di js, anziché salvare un’associazione).

 (defn add-up-faster "Adds up numbers from 1 to n" ([n] (add-up-faster n 0 0)) ([^long n ^long i ^long sum] (if (< ni) sum (recur n (unchecked-inc i) (unchecked-add i sum))))) 

Non direttamente correlato al problema di ottimizzazione, ma il tuo Fib può essere facilmente accelerato:

 (defn fib "Fib" [n] (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))) 

cambia in:

 (def fib (memoize (fn [n] (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))))) 

Funziona molto più velocemente (da 13000 ms per fib 38 su core i5 - perché il mio computer è più lento di dualcores? - a 0,2 ms). In sostanza non è molto diverso da una soluzione iterativa, anche se ti permette di esprimere il problema in modo ricorsivo al prezzo di qualche memoria.

Giocando, è ansible ottenere alcune prestazioni piuttosto buone per fib e utilizzando qualcosa come il seguente:

 (defn fib [^long n] (if (< n 2) n (loop [i 2 l '(1 1)] (if (= in) (first l) (recur (inc i) (cons (+' (first l) (second l)) l)))))) (dotimes [_ 10] (time (fib 51))) ; on old MB air, late 2010 ; "Elapsed time: 0.010661 msecs" 

Questo è un modo più appropriato per node.js per gestire questo:

 Number.prototype.triangle = function() { return this * (this + 1) /2; } var start = new Date(); var result = 100000000 .triangle(); var elapsed = new Date() - start; console.log('Answer is', result, ' in ', elapsed, 'ms'); 

cedendo:

 $ node triangle.js Answer is 5000000050000000 in 0 ms