Является ли NodeJ быстрее, чем Clojure? - PullRequest
31 голосов
/ 02 июня 2011

Я только начал изучать 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

Затем я попытался попробовать классический фиб (после прочтения this ):

в ближайшем будущем:

(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

Примечание. Я использую (time (fib 40)) в clojure, поэтому он игнорирует время загрузкидля JVM.Они работают на MacBook Air (Intel Core 2 Duo 1,86 ГГц).

Так что же заставляет Clojure работать медленно?И почему люди говорят, что «Clojure быстр»?

Заранее спасибо и, пожалуйста, никаких огненных войн.

Ответы [ 7 ]

48 голосов
/ 02 июня 2011
(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,66 ГГц i7 Macbook Pro OS X 10,7 JDK 7 64 бит

Как вы видите, Node.js имеет проблемы.Это с альфа-версией 1.3.0, но вы можете добиться того же в 1.2.0, если знаете, что делаете.

На моей машине Node.js 0.4.8 для addup 1e8 был ~ 990мсдля фиби 41 ~ 7600мс.

            Node.js  | Clojure
                     |
 add-up       990ms  |   130ms
                     |
 fib(41)     7600ms  |  1180ms
38 голосов
/ 02 июня 2011

Я бы действительно ожидал, что Clojure будет значительно быстрее, чем Javascript, если вы оптимизируете свой код для повышения производительности.

Clojure будет статически компилироваться в довольно оптимизированный байт-код Java, когда вы дадите достаточно информации о статических типах (например, подсказки типов или приведение к примитивным типам).По крайней мере, теоретически, вы должны быть в состоянии приблизиться к чистой скорости Java, которая сама по себе очень близка к производительности собственного кода.

Так что давайте докажем это!

В этом случае у вас есть несколько проблем, из-за которых код Clojure работает медленно:

  • Clojure по умолчанию поддерживает арифметику произвольной точности, поэтому любые арифметические операции автоматически проверяются напереполнение и, если необходимо, числа повышаются до BigIntegers и т. д. Эта дополнительная проверка добавляет небольшое количество служебных данных, которое обычно незначительно, но может проявиться, если вы выполняете арифметические операции в узком цикле, подобном этому.Самый простой способ исправить это в Clojure 1.2 - это использовать функции unchecked- * (это немного не элегантно, но будет значительно улучшено в Clojure 1.3)
  • Если вы не укажете иначе, Clojure ведет себя динамически и блокируетаргументы функции.Поэтому я подозреваю, что ваш код создает и упаковывает много Integer / Longs.Способ удалить это для переменных цикла - использовать подсказки примитивного типа и использовать такие конструкции, как loop / recur.
  • Аналогично, n упакован, что означает, что вызов функции <= не может быть оптимизирован дляиспользовать примитивную арифметику.Этого можно избежать, приведя n к длинному примитиву с локальным разрешением. </li>
  • (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)))

Если я сделаю выше, я получу 6мс для решения Clojure в Clojure 1.2.Это примерно в 15-20 раз быстрее, чем код Node.js, и, возможно, в 80-100 раз быстрее, чем ваша оригинальная версия Clojure.

Кстати, это также почти так же быстро, как я могу заставить этот цикл работать чистоJava, поэтому я сомневаюсь, что можно было бы улучшить это на любом языке JVM.Это также ставит нас примерно в 2 машинных цикла на одну итерацию ... так что это, вероятно, недалеко от собственной скорости машинного кода!

(извините, не могу сравнить с Node.js на моей машине, но это 3,3 ГГц ядро ​​i7 980X для всех, кто заинтересован)

26 голосов
/ 02 июня 2011

Комментарий высокого уровня.Node.js и Clojure имеют совершенно разные модели для достижения масштабируемости и, в конечном итоге, для обеспечения быстрой работы программного обеспечения.

Clojure обеспечивает масштабируемость благодаря многоядерному параллелизму.Если вы правильно строите свои программы Clojure, вы можете разделить свою вычислительную работу (через pmap и т. Д.), Чтобы в конечном итоге выполнять параллельно на отдельных ядрах.

Node.js не параллелен.Скорее, его ключевой момент заключается в том, что масштабируемость (обычно в среде веб-приложений) связана с вводом / выводом.Таким образом, Node.js и технология Google V8 достигают масштабируемости благодаря множеству обратных вызовов асинхронного ввода / вывода.

Теоретически, я ожидаю, что Clojure превзойдет Node.js в областях, легко распараллеливаемых.Фибоначчи попадет в эту категорию и победит Node.js, если ему будет достаточно ядер.И Node.js будет лучше для серверных приложений, которые делают много запросов к файловой системе или сети.

В заключение, я не думаю, что это может быть очень хорошим эталоном для сравнения Clojure с Node.js..

6 голосов
/ 02 июня 2011

Пара подсказок, если вы используете clojure 1.2

  • повторение (time ...) тестов, вероятно, увеличит скорость в clojure из-за запуска JIT-оптимизации.
  • (inc i) - немного - быстрее, чем (+ i 1)
  • функции unchecked- * также быстрее (иногда НАМНОГО быстрее), чем их проверенные варианты. Предполагая, что вам не нужно превышать лимит long или double, использование unchecked-add, unchecked-int и т. Д. Может быть намного быстрее.
  • читать описания типов; в некоторых случаях они также могут существенно улучшить скорость.

Clojure 1.3, как правило, быстрее с цифрами, чем 1,2, но он все еще находится в стадии разработки.

Следующее примерно в 20 раз быстрее вашей версии, и его все еще можно улучшить, изменив алгоритм (обратный отсчет, как в версии js, вместо up сохраняет привязку).

(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)))))
2 голосов
/ 04 июня 2011

Непосредственно не связана с проблемой оптимизации, но ваш 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)))))))

Работает намного быстрее (от 13000 мс для фиб38 на ядре i5 - почему мой компьютер медленнее, чем DualCore? - до 0,2 мс).По сути, оно мало чем отличается от итеративного решения - хотя оно позволяет рекурсивно выразить проблему по цене некоторой памяти.

1 голос
/ 04 января 2015

Играя вокруг, вы можете получить довольно хорошую производительность для фиби, используя что-то вроде следующего:

(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"
0 голосов
/ 19 августа 2011

Это более подходящий способ 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...