NodeJs比Clojure更快吗?

时间:2011-06-02 15:47:36

标签: javascript performance node.js clojure

我刚开始学习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很快”?

提前致谢,拜托,没有火焰战争。

8 个答案:

答案 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代码运行缓慢的问题:

  • Clojure默认支持任意精度算术,因此任何算术运算都会自动检查溢出,如果需要,数字会被提升为BigIntegers等。这种额外的检查会增加少量的开销,这通常可以忽略不计,但如果有的话,可以显示你在像这样的紧密循环中运行算术运算。在Clojure 1.2中解决这个问题的简单方法是使用unchecked- *函数(这有点不优雅,但在Clojure 1.3中会得到很大改进)
  • 除非你另有说明,否则Clojure会动态行为并且会设置函数参数。因此我怀疑你的代码正在创建和装箱很多整数/长期。为循环变量删除它的方法是使用原始类型提示并使用loop / recur等构造。
  • 同样,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中获得更高的速度,因为JIT优化正在进行中。
  • (inc i) - 比(+ i 1)
  • 快一点
  • unchecked- *函数也比它们检查的变体更快(有时快很多)。假设您不需要超过long或double的限制,使用unchecked-add,unchecked-int等可能会快得多。
  • 阅读类型声明;在某些情况下,它们也可以大大提高速度。

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