简体   繁体   中英

In Common Lisp, Is there a performance difference between functions and macros?

Consider the following two definitions:

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

In my limited understanding, "running" a function would be faster than a macro, since "running a macro" also involves code expansion. However, I get the following results with 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

Why is this the case?

Is there a way to get it to expand multiple times?

As a matter of fact, yes.

Here is an example, first the case that is usually expected when using macros, ie macros being expanded just once before evaluation:

; 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

Now, switch to interpreted mode:

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" 

The interpret mode can be useful wrt macros if you want to develop a macro and you don't want to recompile all callers every time you update your code.

There is however a performance penaltly, so I don't think benchmarks will be relevant. Besides, the question you asked originally is comparing apples to oranges, since the purpose of a macro is quite different from the purpose of a function.

This question betrays some confusion, and I think it's worth an answer which tries to address this confusion.

First of all, macros and functions do not play the same role in Lisp code, and if you are wondering which to use in a given case you are almost certainly making a mistake.

  • Functions (perhaps more correctly known as procedures , as they may not compute functions) are the things which do run-time computation: they have arguments, return results and may have side-effects. And they have some run-time cost, including possible fixed overheads for calling them. There are some tricks for reducing that fixed cost, and also some tricks which allow you to detect & optimise special cases: see below. They also have some compile-time cost: the compiler is not instantaneous. The compile-time cost of a function can usually be amortised over very many calls to it at run-time, and be treated as asymptotically zero. This is not always correct: for instance when developing programs in an interactive environment you may care a lot about compile-time cost.
  • Macros are functions which take a bit of source code as their argument and compute another bit of source code: their expansion. The function which does the expansion of a macro (the thing that defmacro defines and that you can get at with macro-function &c) is called at compile-time , not run-time. This means that all the expansion cost of a macro is part of the compile-time cost of the program and so, if the program is run many times, the expansion cost of a macro becomes asymptotically zero. The run-time cost of the macro is the cost of evaluating the code it returned, because there are no macros in compiled code : they have all been expanded by the compiler leaving only their expansions in the code.

It's pretty clear from this firstly that functions and macros play essentially different roles in programs – functions do run-time computation while macros let you extend the language – and secondly that the run-time cost of macros is zero.

There are two reasons that things may be more complicated than this.

The first is that, long ago in the prehistory of Lisp, people wanted ways to optimise small functions: functions where the fixed overhead of calling the function was large enough to matter. And Lisp compilers were primitive things which did not offer facilities to do this and were not smart enough to do it themselves. And, of course, it turns out that you can do this with macros: you can abuse the facilities macros give you to compute source-code transformations to implement inline functions. And people did this.

But that was long ago: Common Lisp provides two facilities which remove the need for this.

  • You can declare functions as inline which drops a huge hint to the compiler that it should inline calls to them. And you can do this without having to alter the definition of the function at all: you just add suitable (declaim (inline ...)) s in the code and any reasonable compiler will do the inlining for you.
  • You can define compiler macros , which are a special sort of macro associated with a function which the compiler will invoke, at compile time, and which is allowed to, for instance, detect particularly simple invocations of the function and optimise them away, while punting on more complicated calls. Again, compiler macros do not interfere with the normal function definition at all, although they should be careful to expand to code which is equivalent to the function they are a compiler macro for.

As well as this, modern Lisp compilers are a lot cleverer than antique ones (no-one now thinks compiling Lisp is so hard we need special smart hardware so we can stick with a dumb compiler), and they will often do a really good job of optimising simple calls, especially to functions defined in the CL standard, themselves.

The second reason that things may be more complicated is that run-time and compile-time may not always be distinct. If you are, for instance, writing a program that writes programs (beyond just writing macros, which are simple cases of this) then the sequence of events can get quite tangled (for instance compile-run-read-metacompile-compile-run). In this case macro expansion can happen at various times, and you may end up with essentially your own meta-macro system associated with the metacompilation process. That's outside the scope of this answer.

In Lisp there was the need for code transformations. For example one can implement new controls structures with it.

Imagine we want to exchange the if clauses in a not if :

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

One of the original attempts to provide these transformations were so-called FEXPR functions: Functions which get their arguments unevaluated. The FEXPR function can then decide what to with the arguments and which to evaluate under which circumstances.

This works okay when using a Lisp interpreter - an evaluator which interprets Lisp code directly. But it's not clear how to compile such code.

Macro usage in code OTOH can be compiled efficiently:

  • the code gets expanded
  • the expanded code gets compiled

Thus one of the reasons we use macros (and not FEXPRs) is that they are getting expanded at compile time and there is no overhead at runtime.

For a Lisp interpreter, the macro would be expanded at runtime.

"running" a function would be faster than a macro, since "running a macro" also involves code expansion

Only if macros were to be expanded at runtime. But in compiled code this is not the case.

Thanks to Scott Hunter for pointing out.

The macro is expanded just once - this can be checked by

(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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM