NodeJ比Clojure更快吗?

我刚开始学习Clojure。 我注意到的第一件事就是没有循环。 没关系,我可以复发。 那么让我们来看看这个函数(来自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))))) 

为了在Javascript中实现相同的function,我们使用如下循环:

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

定时时,结果如下所示:

 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 

然后我继续尝试经典的fib(阅读后):

在clojure:

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

在js中:

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

再次,performance有一些不同。

 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 

注意我在clojure中使用(time(fib 40)),所以它忽略了JVM的启动时间。 这些在MacBook Air(1.86 Ghz Intel Core 2 Duo)上运行。

那么是什么导致Clojure在这里变慢呢? 为什么人们说“Clojure速度很快”?

在此先感谢,请不要火焰战争。

 (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))) ) 

所有数字从2.66ghz i7的MacBook Pro OS X 10.7 JDK 7 64位

正如你可以看到的Node.js是痛苦的。 这与1.3.0 alphas,但你可以在1.2.0中实现相同的东西,如果你知道你在做什么。

在我的机器上Node.js 0.4.8 for addup 1e8是〜990ms,fib为41〜7600ms。

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

实际上我希望Clojure比Javascript更快,如果你优化你的代码的性能。

只要您提供足够的静态types信息(即键入提示或转换为基本types),Clojure就会静态编译为相当优化的Java字节码。 所以至less在理论上,你应该能够接近纯粹的Java速度,它本身非常接近本地代码的性能。

所以我们来certificate一下吧!

在这种情况下,你有几个问题导致Clojure代码运行缓慢:

  • Clojure默认支持任意精度算术,所以任何算术运算都会自动检查溢出,如果必要的话,数字会被提升到BigIntegers等等。这个额外的检查会增加一些通常可以忽略不计的开销,但是如果运行algorithm像这样在一个紧密的循环中操作。 在Clojure 1.2中解决这个问题的简单方法是使用unchecked- *函数(这有点不雅观,但在Clojure 1.3中会有很大的改进)
  • 除非另有说明,否则Clojure会dynamic地执行,并将函数参数框出来。 因此,我怀疑你的代码是创build和装箱了很多整数/长整数。 去除循环variables的方法是使用基本types提示并使用循环/循环等结构。
  • 同样, n是装箱的,这意味着<=函数调用不能被优化以使用基本算术。 你可以通过将n转换成一个本地let的长基元来避免这种情况。
  • (time (some-function))也是在Clojure中进行基准testing的一种不可靠的方式,因为它不一定会让JIT编译优化起作用。您经常需要先运行(某些函数)几次,以便JIT具有一个机会去做它的工作。

我对优化的Clojure加法版本的build议因此更像是:

 (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)))) 

更好的方法是如下(允许JIT编译发生):

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

如果我这样做,Clojure 1.2中的Clojure解决scheme可以获得6毫秒的时间。 这比Node.js代码快15-20倍,比原来的Clojure版本快80-100倍。

顺便说一句,这也是我可以得到这个循环去纯Java的速度,所以我怀疑,有可能在任何JVM语言中提高这一点。 它也使我们每次迭代约2个机器周期…所以它可能离原生机器码速度也不远!

(遗憾的是无法在我的机器上与Node.js进行基准testing,但对于任何感兴趣的人来说,它都是3.3 GHz核心i7 980X)

高层评论。 Node.js和Clojure具有完全不同的模型来获得可伸缩性,并最终使软件运行得更快。

Clojure通过多核并行来实现可扩展性。 如果你正确地构buildClojure程序,你可以分配你的计算工作(通过pmap等),最终在不同的核心上并行运行。

Node.js不是并行的。 相反,其关键的洞察是可伸缩性(通常在Web应用程序环境中)是I / O绑定的。 所以Node.js和Google V8技术通过许多asynchronousI / Ocallback获得了可扩展性。

从理论上讲,我希望Clojure能够在易于并行化的领域击败Node.js。 如果给出足够的核心,斐波纳契将落入这个类别,并击败Node.js。 Node.js对于向文件系统或networking发出很多请求的服务器端应用程序会更好。

总之,我不认为这可能是比较Clojure和Node.js的很好的基准。

几个提示,假设你正在使用clojure 1.2

  • 重复(时间…)testing可能会获得更高的速度clojure,因为JIT优化踢。
  • (inc i)比 – (+ i 1)快一点 –
  • unchecked- *函数也比其检查的变体更快(有时更快)。 假设你不需要超过long或double的限制,使用unchecked-add,unchecked-int等可能会快很多。
  • 阅读types声明; 在某些情况下,它们也可以大幅度提高速度。

Clojure 1.3的数值通常比1.2快,但仍在开发中。

以下是比你的版本快20倍,它仍然可以通过修改algorithm(倒计时,像js版本一样,而不是保存绑定)来改进。

 (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))))) 

与目前的优化问题没有直接关系,但是您的Fib可以很容易地加快:

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

改成:

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

工作速度要快得多(从核心i5上的fib38开始,13000 ms – 为什么我的电脑比dualcores慢? – 0.2 ms)。 从本质上说,它与迭代解决scheme没有多大区别 – 尽pipe它允许您以某种内存的价格recursion的方式expression问题。

玩耍,你可以得到一些相当不错的performance,以及使用类似下面的东西:

 (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" 

这是一个更合适的node.js方法来处理这个问题:

 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'); 

收益:

 $ node triangle.js Answer is 5000000050000000 in 0 ms