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