Является ли NodeJs быстрее, чем Clojure?
Я только начал изучать Clojure. Одна из первых вещей, которые я заметил, это отсутствие циклов. Хорошо, я могу повториться. Поэтому давайте посмотрим на эту функцию (из Практического 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):
в 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
Примечание. Я использую (время (fib 40)) в clojure, поэтому игнорирует время загрузки для JVM. Они запускаются на MacBook Air (1,86 ГГц Intel Core 2 Duo).
Итак, что заставляет clojure быть медленным здесь? И почему люди говорят, что "Clojure быстро"?
Спасибо заранее и пожалуйста, никаких пламенных войн.
Ответы
Ответ 1
(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 альфа, но вы можете добиться того же в 1.2.0, если знаете, что делаете.
На моей машине Node.js 0.4.8 для дополнения 1e8 было ~ 990 мс, для фила 41 ~ 7600 мс.
Node.js | Clojure
|
add-up 990ms | 130ms
|
fib(41) 7600ms | 1180ms
Ответ 2
Я бы ожидал, что Clojure будет значительно быстрее, чем Javascript, если вы оптимизируете свой код для производительности.
Clojure будет статически компилироваться в довольно оптимизированный байт-код Java всякий раз, когда вы даете достаточно информации о статическом типе (т.е. вводите типы или отбрасываете примитивные типы). Поэтому, по крайней мере, теоретически, вы должны быть достаточно близки к чистой скорости Java, что само по себе довольно близко к производительности собственного кода.
Итак, пусть это докажет!
В этом случае у вас есть несколько проблем, вызывающих медленный запуск Clojure:
- Clojure поддерживает арифметику произвольной точности по умолчанию, поэтому любые арифметические операции автоматически проверяются на переполнение и при необходимости увеличиваются числа до BigIntegers и т.д. Эта дополнительная проверка добавляет небольшое количество накладных расходов, которое обычно незначительно, но может показывать если вы выполняете арифметические операции в таком замкнутом цикле. Простым способом исправить это в Clojure 1.2 является использование непроверенных-* функций (это немного неэлегантно, но будет значительно улучшено в Clojure 1.3)
- Если вы не скажете об этом иначе, Clojure ведет себя динамически, а аргументы функции аргументов. Поэтому я подозреваю, что ваш код создает и боксирует много целых чисел. Способ удалить это для ваших переменных цикла - использовать примитивные подсказки типа и использовать конструкции, такие как loop/recur.
- Аналогично,
n
помещается в квадрат, что означает, что вызов <= function не может быть оптимизирован для использования примитивной арифметики. Вы можете избежать этого, переведя n в длинный примитив с локальным let.
-
(time (some-function))
также является ненадежным способом тестирования в Clojure, потому что это не обязательно позволит оптимизировать компиляцию JIT. Часто вам нужно сначала запустить (некоторые функции), чтобы JIT шанс выполнить свою работу.
Мое предложение для оптимизированной версии add-up 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,7 ГГц i7 980X для всех, кто интересуется)
Ответ 3
Комментарий высокого уровня. Node.js и Clojure имеют совершенно разные модели для достижения масштабируемости и в конечном итоге ускоряют работу программного обеспечения.
Clojure обеспечивает масштабируемость через многоядерный parallelism. Если вы правильно создадите свои программы Clojure, вы можете разделить свою вычислительную работу (через pmap
и т.д.), Чтобы в конечном счете работать параллельно на отдельных ядрах.
Node.js не является параллельным. Скорее его ключевое понимание заключается в том, что масштабируемость (как правило, в среде веб-приложения) связана с привязкой ввода-вывода. Таким образом, технология Node.js и Google V8 обеспечивает масштабируемость через многие асинхронные обратные вызовы ввода-вывода.
Теоретически, я ожидал бы, что Clojure будет бить Node.js в областях, которые легко распараллеливаются. Фибоначчи попадет в эту категорию и будет бить Node.js, если будет дано достаточно ядер. И Node.js будет лучше для серверных приложений, которые делают много запросов к файловой системе или сети.
В заключение я не думаю, что это может быть очень хорошим ориентиром для сравнения Clojure и Node.js.
Ответ 4
Несколько советов, предполагая, что вы используете clojure 1.2
- Повторение тестов (время...), вероятно, приведет к увеличению скорости в clojure, поскольку оптимизация JIT начнется.
- (inc i) - немного - быстрее, чем (+ я 1)
- функции unchecked-* также быстрее (иногда МНОГО быстрее), чем их проверенные варианты. Предполагая, что вам не нужно превышать лимит длин или удвоений, использование 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)))))
Ответ 5
Не связано непосредственно с проблемой оптимизации, но ваш 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 - почему мой компьютер медленнее, чем dualcores? - до 0,2 мс). По сути, это не сильно отличается от итеративного решения - хотя это позволяет вам рекурсивно выражать проблему по цене какой-либо памяти.
Ответ 6
Играя, вы можете получить неплохую производительность для фиб, используя что-то вроде ниже:
(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"
Ответ 7
Это более подходящий способ 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