我刚开始学习Clojure。我注意到的第一件事就是没有循环。没关系,我可以重复一遍。那么让我们来看看这个函数(来自Practical Clojure):
(defn add-up
"Adds up numbers from 1 to n"
([n] (add-up n 0 0))
([n i sum]
(if (< n i)
sum
(recur n (+ 1 i) (+ i sum)))))
为了在Javascript中实现相同的功能,我们使用如下循环:
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(阅读this后):
in 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);
}
同样,表现有很大不同。
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很快”?
提前致谢,拜托,没有火焰战争。
答案 0 :(得分:48)
(set! *unchecked-math* true)
(defn add-up ^long [^long n]
(loop [n n i 0 sum 0]
(if (< n i)
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 64bit
正如您所看到的,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
答案 1 :(得分:38)
如果您优化代码以提高性能,我实际上希望Clojure比Javascript快得多。
只要提供足够的静态类型信息(即对基本类型进行类型提示或转换),Clojure将静态编译为相当优化的Java字节码。所以至少在理论上,你应该能够非常接近纯Java速度,这本身非常接近本机代码性能。
所以让我们证明一下吧!
在这种情况下,您有几个导致Clojure代码运行缓慢的问题:
n
被装箱,这意味着无法优化&lt; =函数调用以使用原始算术。您可以通过将n转换为带有本地let的长基元来避免这种情况。(time (some-function))
也是在Clojure中进行基准测试的一种不可靠的方法,因为它不一定允许JIT编译优化启动。你经常需要先运行几次(某些函数)以便JIT具有有机会继续工作。我对优化的Clojure版本的加法的建议因此更像是:
(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中获得 6 ms 的Clojure解决方案。这比Node.js代码快15-20倍,可能比原始Clojure版本快80-100倍。
顺便说一下,这也和我在纯Java中使用这个循环一样快,所以我怀疑在任何JVM语言中都可以改进这一点。它还使我们每次迭代大约2个机器周期......所以它可能与原生机器代码速度相差不远!
(抱歉无法在我的机器上对Node.js进行基准测试,但对于任何感兴趣的人来说,它都是3.3 GHz核心i7 980X)
答案 2 :(得分:26)
高级评论。 Node.js和Clojure拥有完全不同的模型,可以实现可扩展性并最终使软件快速运行。
Clojure通过多核并行实现可扩展性。如果你正确地构建你的Clojure程序,你可以分配你的计算工作(通过pmap
等),最终在不同的核心上并行运行。
Node.js不是并行的。相反,它的关键洞察力是可扩展性(通常在Web应用程序环境中)受I / O限制。因此Node.js和Google V8技术通过许多异步I / O回调实现了可扩展性。
理论上,我希望Clojure在易于并行化的区域中击败Node.js。 Fibonacci属于这一类,如果有足够的内核,它将击败Node.js。对于向文件系统或网络发出大量请求的服务器端应用程序,Node.js会更好。
总之,我不认为这可能是比较Clojure和Node.js的非常好的基准。
答案 3 :(得分:6)
一些提示,假设你使用的是clojure 1.2
Clojure 1.3通常在数字上比1.2更快,但它仍在开发中。
以下版本比您的版本快20倍,并且仍然可以通过修改算法来改进(倒计时,就像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 (< n i)
sum
(recur n (unchecked-inc i) (unchecked-add i sum)))))
答案 4 :(得分:2)
与手头的优化问题没有直接关系,但您的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上的fib 38起13000毫秒 - 为什么我的计算机比双核慢? - 到0.2毫秒)。从本质上讲,它与迭代解决方案没有多大区别 - 尽管它确实允许您以递归的方式表达问题以获得某些内存的价格。
答案 5 :(得分:1)
在游戏中,您可以使用以下内容获得一些非常好的性能:
(defn fib [^long n]
(if (< n 2)
n
(loop [i 2 l '(1 1)]
(if (= i n)
(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"
答案 6 :(得分:0)
这个问题需要在 2021 年更新。
Node.js v14.17 | Clojure v1.10(基于 Java 1.8) |
---|---|
2.403s |
963.443556 ms |
function fib (n) {
if (n <= 1) return 1;
return fib(n-1) + fib(n-2);
}
console.time('foo')
fib(40)
console.timeEnd('foo')
在 Clojure 中
(ns schema
(:require
[clojure.core :refer [time]]
)
(:gen-class))
(defn fib ^long [^long n]
(if (<= n 1) 1
(+ (fib (dec n)) (fib (- n 2)))))
(defn -main
[& args]
(time (fib 40))
)
在 'linux x64 上运行 | 8 个 vCPU | 46.8GB 内存'
答案 7 :(得分:-1)
这是一种更合适的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