Подтвердить что ты не робот

Является ли 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 быстро"?

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

4b9b3361

Ответ 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