В Common Lisp, есть ли разница в производительности между функциями и макросами? - PullRequest
0 голосов
/ 05 июня 2019

Рассмотрим следующие два определения:

(defun fun-add (a b) (+ a b))
(defmacro macro-add (a b) `(+ ,a ,b))

В моем ограниченном понимании «запуск» функции будет быстрее, чем макрос, так как «запуск макроса» также включает расширение кода. Тем не менее, я получаю следующие результаты с SBCL:

CL-USER> (time (loop for i below 1e7
                     do (fun-add 15 25)))
Evaluation took:
  0.180 seconds of real time
  0.179491 seconds of total run time (0.179491 user, 0.000000 system)
  99.44% CPU
  396,303,718 processor cycles
  0 bytes consed

NIL


CL-USER> (time (loop for i below 1e7
                     do (macro-add 15 25)))
Evaluation took:
  0.034 seconds of real time
  0.033719 seconds of total run time (0.033719 user, 0.000000 system)
  100.00% CPU
  74,441,518 processor cycles
  0 bytes consed

NIL

Почему это так?

Ответы [ 4 ]

4 голосов
/ 05 июня 2019

Есть ли способ заставить его расширяться в несколько раз?

На самом деле, да.

Вот пример, во-первых, случай, который обычно ожидается при использовании макросов, то есть макросы раскрываются только один раз перед оценкой:

; SLIME 2.23
CL-USER> (defmacro test () (print "EXPANDING"))
TEST
CL-USER> (test)

"EXPANDING" ;; printed
"EXPANDING" ;; return value

CL-USER> (dotimes (i 10) (test))

"EXPANDING" 
NIL

Теперь переключитесь на интерпретированный режим:

CL-USER> (setf sb-ext:*evaluator-mode* :interpret)
:INTERPRET

CL-USER> (dotimes (i 10) (test))

"EXPANDING"
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 

Режим интерпретации может быть полезен w.r.t. макросы, если вы хотите разработать макрос и не хотите перекомпилировать всех вызывающих каждый раз, когда обновляете свой код.

Тем не менее, производительность снижается, так что я не думаю, что эталонные тесты будут актуальны. Кроме того, первоначально заданный вами вопрос заключается в сравнении яблок с апельсинами, поскольку назначение макроса сильно отличается от назначения функции.

3 голосов
/ 05 июня 2019

Этот вопрос выдает некоторую путаницу, и я думаю, что стоит ответить на него, пытаясь устранить эту путаницу.

Прежде всего, макросы и функции не играют одинаковой роли в коде на Лиспе, и если вам интересно, какой из них использовать в данном случае, вы почти наверняка совершаете ошибку.

  • Функции (возможно, более правильно известные как процедуры , поскольку они могут не вычислять функции) - это те вещи, которые выполняют вычисления во время выполнения: они имеют аргументы, возвращают результаты и могут иметь побочные эффекты. И у них есть некоторые затраты времени выполнения, включая возможные фиксированные накладные расходы для их вызова. Существуют некоторые приемы снижения фиксированной стоимости, а также некоторые приемы, позволяющие выявлять и оптимизировать особые случаи: см. Ниже. У них также есть некоторая стоимость времени компиляции: компилятор не мгновенный. Стоимость времени компиляции функции обычно амортизируется из-за очень большого количества обращений к ней во время выполнения и может рассматриваться как асимптотически нулевая. Это не всегда правильно: например, при разработке программ в интерактивной среде вы можете сильно беспокоиться о стоимости времени компиляции.
  • Макросы - это функции, которые принимают в качестве аргумента бит исходного кода и вычисляют еще один бит исходного кода: их расширение. Функция, которая выполняет расширение макроса (то, что определяет defmacro и который можно получить с помощью macro-function & c), вызывается в время компиляции , а не время выполнения. Это означает, что все затраты на развертывание макроса являются частью времени компиляции программы , и поэтому, если программа запускается много раз, стоимость расширения макроса становится асимптотически равной нулю. Стоимость выполнения макроса - это стоимость вычисления кода, который он возвратил, потому что в скомпилированном коде нет макросов : все они были расширены компилятором, оставив только свои расширения в коде.

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

Есть две причины, по которым вещи могут быть более сложными, чем эта.

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

Но это было давно: Common Lisp предоставляет две возможности, которые устраняют необходимость в этом.

  • Вы можете объявить функции как inline , что дает компилятору огромный намек на то, что он должен встроить вызовы к ним. И вы можете сделать это без необходимости изменять определение функции: вы просто добавляете подходящие (declaim (inline ...)) s в код, и любой разумный компилятор сделает вставку за вас.
  • Вы можете определить макросы компилятора , которые представляют собой особый вид макроса, связанного с функцией, которую компилятор будет вызывать во время компиляции, и которая позволяет, например, обнаруживать особенно простые вызовы функции и оптимизировать их, в то же время, используя более сложные вызовы. Опять же, макросы компилятора вообще не мешают определению нормальной функции, хотя они должны быть осторожны при расширении до кода, который эквивалентен функции, для которой они являются макросом компилятора.

Кроме того, современные компиляторы Lisp намного умнее старых (никто сейчас не думает, что компилировать Lisp так сложно, что нам нужно специальное интеллектуальное оборудование, чтобы мы могли придерживаться тупого компилятора), и они часто будут делать действительно хорошая работа по оптимизации простых вызовов, особенно самих функций, определенных в стандарте CL.

Вторая причина, по которой вещи могут быть более сложными, заключается в том, что время выполнения и время компиляции не всегда различаются. Если вы, например, пишете программу, которая пишет программы (помимо простого написания макросов, которые являются простыми случаями этого), тогда последовательность событий может быть довольно запутанной (например, compile-run-read-metacompile-compile-run) , В этом случае расширение макроса может происходить в разное время, и вы можете в конечном итоге получить собственную метамакросистему, связанную с процессом метакомпиляции. Это выходит за рамки этого ответа.

2 голосов
/ 06 июня 2019

В Лиспе была необходимость в преобразованиях кода.Например, можно реализовать с его помощью новые управляющие структуры.

Представьте, что мы хотим обменять предложения if на , если :

(defmacro nif (test else then)
  `(if ,test ,then ,else))

Один изПервоначальными попытками обеспечить эти преобразования были так называемые функции FEXPR: функции, которые получают свои аргументы без оценки.Затем функция FEXPR может решить, что делать с аргументами, а какие оценивать при каких обстоятельствах.

Это работает нормально при использовании интерпретатора Lisp - оценщика, который интерпретирует код Lisp напрямую.Но не ясно, как скомпилировать такой код.

Использование макроса в коде OTOH может быть эффективно скомпилировано:

  • код расширяется
  • расширенный код компилируется

Таким образом, одна из причин, по которой мы используем макросы (а не FEXPR), заключается в том, что они расширяются во время компиляции, и нет никаких накладных расходов во время выполнения.

Для интерпретатора Lisp:макрос будет раскрываться во время выполнения.

«запуск» функции будет быстрее, чем макрос, поскольку «запуск макроса» также включает в себя расширение кода

только еслимакросы должны были быть расширены во время выполнения.Но в скомпилированном коде это не так.

1 голос
/ 05 июня 2019

Спасибо Скотту Хантеру за указание.

Макрос раскрывается только один раз - это можно проверить с помощью

(defvar *macro-count* 0)
(defmacro macro-add (a b) 
  (incf *macro-count*)
  `(+ ,a ,b))
CL-USER> (time (loop for i below 1e8
                     do (macro-add 15 25)))
Evaluation took:
  0.335 seconds of real time
  0.335509 seconds of total run time (0.335509 user, 0.000000 system)
  100.30% CPU
  740,823,874 processor cycles
  0 bytes consed

NIL
CL-USER> *macro-count*
1
...