Почему рекурсивная функция работает лучше, чем итеративная функция в elisp?
В качестве теста для одного из моих классов наш учитель попросил нас протестировать рекурсивный и нерекурсивный подход к известному евклидову алгоритму:
Итерационный
(defun gcdi (a b)
(let ((x a) (y b) r)
(while (not (zerop y))
(setq r (mod x y) x y y r))
x))
Рекурсивный
(defun gcdr (a b)
(if (zerop b)
a
(gcdr b (mod a b))))
И затем я проверил тест:
(defun test-iterative ()
(setq start (float-time))
(loop for x from 1 to 100000
do (gcdi 14472334024676221 8944394323791464)) ; Fibonacci Numbers close to 2^64 >:)
(- (float-time) start))
(defun test-recursive ()
(setq start (float-time))
(loop for x from 1 to 100000
do (gcdr 14472334024676221 8944394323791464)) ; Fibonacci Numbers close to 2^64 >:)
(- (float-time) start))
И затем я запустил таймеры:
(test-recursive)
: 1.359128475189209
(test-iterative)
: 1.7059495449066162
Итак, мой вопрос в том, почему рекурсивный тест выполнялся быстрее, чем итеративный тест? Не итеративно ли почти всегда лучше рекурсии? Является ли elisp исключением?
Ответы
Ответ 1
Теоретический ответ заключается в том, что рекурсивная версия на самом деле tail
рекурсивный и, следовательно, должен компилироваться на итерацию.
Тем не менее, дизассемблирование
функции раскрывают правду:
byte code for gcdi:
args: (a b)
0 varref a
1 varref b
2 constant nil
3 varbind r
4 varbind y
5 varbind x
6 varref y
7:1 constant 0
8 eqlsign
9 goto-if-not-nil 2
12 constant mod
13 varref x
14 varref y
15 call 2
16 varset r
17 varref y
18 varset x
19 varref r
20 dup
21 varset y
22 goto 1
25:2 varref x
26 unbind 3
27 return
против
byte code for gcdr:
args: (a b)
0 varref b
1 constant 0
2 eqlsign
3 goto-if-nil 1
6 varref a
7 return
8:1 constant gcdr
9 varref b
10 constant mod
11 varref a
12 varref b
13 call 2
14 call 2
15 return
Вы можете видеть, что gcdr
имеет почти наполовину столько инструкций, но содержит инструкции two call
, что означает, что ELisp выполняет не, по-видимому, преобразовать хвостовой рекурсивный вызов в итерацию.
Однако вызовы функций в ELisp относительно дешевы и
поэтому рекурсивная версия выполняется быстрее.
PS. Хотя вопрос имеет смысл, и ответ на самом деле вообще применим (например, такой же подход применим для Python и CLISP, inter alia), следует знать, что выбор правильного алгоритма (например, linearithmic merge-sort вместо квадратичного пузырька -sort) гораздо важнее, чем "микро-оптимизации" реализации.
Ответ 2
Хм... действительно, это странно, так как реализация функций Emacs (и, следовательно, рекурсия) не очень эффективна.
Я просто оценил код ниже:
(defun sm-gcdi (a b)
(let ((x a) (y b) r)
(while (not (zerop y))
(setq r (mod x y) x y y r))
x))
(defun sm-gcdr (a b)
(if (zerop b)
a
(sm-gcdr b (mod a b))))
(defun sm-test-iterative ()
(let ((start (float-time)))
(dotimes (_ 100000)
(sm-gcdi 14472334024676221 8944394323791464))
(- (float-time) start)))
(defun sm-test-recursive ()
(let ((start (float-time)))
(dotimes (_ 100000)
(sm-gcdr 14472334024676221 8944394323791464))
(- (float-time) start)))
а затем попробовали M-: (sm-test-recursive)
и M-: (sm-test-iterative)
и, конечно же, итеративная версия для меня быстрее. Затем я сделал M-: (byte-compile 'sm-gcdi)
и M-: (byte-compile 'sm-gcdr)
и попытался снова, а разность скоростей была еще больше.
Итак, ваши измерения стали для меня неожиданностью: они не соответствуют моим ожиданиям и не соответствуют моим тестам.