Почему сокращение этой ленивой последовательности замедляет эту программу Clojure в 20 раз?

У меня есть программа Clojure, которая возвращает сумму ленивой последовательности even Числа Фибоначчи ниже n:

(defn sum-of-even-fibonaccis-below-1 [n]
  (defn fib [a b] (lazy-seq (cons a (fib b (+ b a)))))
  (reduce + (take-while (partial >= n) (take-nth 3 (fib 0 1)))))

(time (dotimes [n 1000] (sum-of-even-fibonaccis-below-1 4000000))) ;; => "Elapsed time: 98.764msecs"

Это не очень эффективно. Но если я не уменьшу последовательность и просто верну список значений (0 2 8 34 144...) он может делать свою работу в 20 раз быстрее:

(defn sum-of-even-fibonaccis-below-2 [n]
  (defn fib [a b] (lazy-seq (cons a (fib b (+ b a)))))
  (take-while (partial >= n) (take-nth 3 (fib 0 1))))


(time (dotimes [n 1000] (sum-of-even-fibonaccis-below-2 4000000))) ;; => "Elapsed time: 5.145msecs"

Почему reduce так дорого обходится этой ленивой последовательности Фибоначчи, и как я могу ускорить ее, не отказываясь от идиоматической Clojure?

2 ответа

Решение

Разница во времени исполнения является результатом лени. В sum-of-even-fibonaccis-below-2 вы производите только ленивый ряд чисел Фибоначчи, который не реализован (dotimes только звонки sum-of-even-fibonaccis-below-2 создать ленивую последовательность, но не оценивает все ее содержимое). Так ведь твой второй time Выражение не возвращает список значений, но ленивый seq, который будет генерировать свои элементы только тогда, когда вы их попросите.

Чтобы заставить реализацию ленивой последовательности вы можете использовать dorun если вам не нужно сохранять его как значение или doall если вы хотите получить реализованный seq (будьте осторожны с inifinite seqs).

Если вы измеряете второй случай с sum-of-even-fibonaccis-below-2 завернут в dorun вы получите время, сопоставимое с sum-of-even-fibonaccis-below-1,

Результаты от моей машины:

(time (dotimes [n 1000] (sum-of-even-fibonaccis-below-1 4000000))) ;; => "Elapsed time: 8.544193 msecs"

(time (dotimes [n 1000] (dorun (sum-of-even-fibonaccis-below-2 4000000)))) ;; => "Elapsed time: 8.012638 msecs"

Я также заметил, что вы определили fib функция с defn внутри другого defn, Вы не должны делать это как defn всегда будет определять функцию на верхнем уровне в вашем пространстве имен. Итак, ваш код должен выглядеть так:

(defn fib [a b] (lazy-seq (cons a (fib b (+ b a)))))

(defn sum-of-even-fibonaccis-below-1 [n]
  (reduce + (take-while (partial >= n) (take-nth 3 (fib 0 1)))))

(defn sum-of-even-fibonaccis-below-2 [n]
  (take-while (partial >= n) (take-nth 3 (fib 0 1))))

Если вы хотите определить локально ограниченную функцию, вы можете взглянуть на letfn,

Комментарий

Вы можете реорганизовать свои функции - и дать им лучшие имена - таким образом:

(defn fib [a b] (lazy-seq (cons a (fib b (+ b a)))))

(defn even-fibonaccis-below [n]
  (take-while (partial >= n) (take-nth 3 (fib 0 1))))

(defn sum-of-even-fibonaccis-below [n]
  (reduce + (even-fibonaccis-below n)))

Это легче понять и, следовательно, ответить.

Другие вопросы по тегам