Этот вопрос выдает некоторую путаницу, и я думаю, что стоит ответить на него, пытаясь устранить эту путаницу.
Прежде всего, макросы и функции не играют одинаковой роли в коде на Лиспе, и если вам интересно, какой из них использовать в данном случае, вы почти наверняка совершаете ошибку.
- Функции (возможно, более правильно известные как процедуры , поскольку они могут не вычислять функции) - это те вещи, которые выполняют вычисления во время выполнения: они имеют аргументы, возвращают результаты и могут иметь побочные эффекты. И у них есть некоторые затраты времени выполнения, включая возможные фиксированные накладные расходы для их вызова. Существуют некоторые приемы снижения фиксированной стоимости, а также некоторые приемы, позволяющие выявлять и оптимизировать особые случаи: см. Ниже. У них также есть некоторая стоимость времени компиляции: компилятор не мгновенный. Стоимость времени компиляции функции обычно амортизируется из-за очень большого количества обращений к ней во время выполнения и может рассматриваться как асимптотически нулевая. Это не всегда правильно: например, при разработке программ в интерактивной среде вы можете сильно беспокоиться о стоимости времени компиляции.
- Макросы - это функции, которые принимают в качестве аргумента бит исходного кода и вычисляют еще один бит исходного кода: их расширение. Функция, которая выполняет расширение макроса (то, что определяет
defmacro
и который можно получить с помощью macro-function
& c), вызывается в время компиляции , а не время выполнения. Это означает, что все затраты на развертывание макроса являются частью времени компиляции программы , и поэтому, если программа запускается много раз, стоимость расширения макроса становится асимптотически равной нулю. Стоимость выполнения макроса - это стоимость вычисления кода, который он возвратил, потому что в скомпилированном коде нет макросов : все они были расширены компилятором, оставив только свои расширения в коде.
Из этого, во-первых, совершенно очевидно, что функции и макросы играют в программах существенно разные роли - функции выполняют вычисления во время выполнения, в то время как макросы позволяют расширять язык - и, во-вторых, стоимость макросов во время выполнения равна нулю.
Есть две причины, по которым вещи могут быть более сложными, чем эта.
Во-первых, давным-давно в предыстории Lisp люди хотели способы оптимизации небольших функций: функций, в которых фиксированные накладные расходы на вызов функции были достаточно большими, чтобы иметь значение. И компиляторы Lisp были примитивными вещами, которые не предлагали возможности сделать это и не были достаточно умны, чтобы сделать это сами. И, конечно же, оказывается, что вы можете делать это с помощью макросов: вы можете злоупотреблять средствами, которые предоставляют макросы для вычисления преобразований исходного кода для реализации встроенных функций. И люди сделали это.
Но это было давно: Common Lisp предоставляет две возможности, которые устраняют необходимость в этом.
- Вы можете объявить функции как inline , что дает компилятору огромный намек на то, что он должен встроить вызовы к ним. И вы можете сделать это без необходимости изменять определение функции: вы просто добавляете подходящие
(declaim (inline ...))
s в код, и любой разумный компилятор сделает вставку за вас.
- Вы можете определить макросы компилятора , которые представляют собой особый вид макроса, связанного с функцией, которую компилятор будет вызывать во время компиляции, и которая позволяет, например, обнаруживать особенно простые вызовы функции и оптимизировать их, в то же время, используя более сложные вызовы. Опять же, макросы компилятора вообще не мешают определению нормальной функции, хотя они должны быть осторожны при расширении до кода, который эквивалентен функции, для которой они являются макросом компилятора.
Кроме того, современные компиляторы Lisp намного умнее старых (никто сейчас не думает, что компилировать Lisp так сложно, что нам нужно специальное интеллектуальное оборудование, чтобы мы могли придерживаться тупого компилятора), и они часто будут делать действительно хорошая работа по оптимизации простых вызовов, особенно самих функций, определенных в стандарте CL.
Вторая причина, по которой вещи могут быть более сложными, заключается в том, что время выполнения и время компиляции не всегда различаются. Если вы, например, пишете программу, которая пишет программы (помимо простого написания макросов, которые являются простыми случаями этого), тогда последовательность событий может быть довольно запутанной (например, compile-run-read-metacompile-compile-run) , В этом случае расширение макроса может происходить в разное время, и вы можете в конечном итоге получить собственную метамакросистему, связанную с процессом метакомпиляции. Это выходит за рамки этого ответа.