请考虑以下两个定义:
(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
为什么会这样?
答案 0 :(得分:4)
有没有办法使它扩展多次?
事实上,是的。
这里是一个例子,首先是在使用宏时通常会遇到的情况,即在评估之前宏只被扩展了一次:
; 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"
解释模式可能有用宏,如果您想开发一个宏并且不想在每次更新代码时都重新编译所有调用者。
但是,这会带来性能上的损失,因此我认为基准测试并不重要。此外,您最初提出的问题是将苹果与桔子进行比较,因为宏的用途与函数的用途完全不同。
答案 1 :(得分:3)
这个问题出了一些混乱,我认为值得尝试解决这个混乱的问题。
首先,宏和函数在Lisp代码中的作用不同,如果您想知道在给定情况下使用哪种宏,几乎肯定会犯错。
defmacro
定义并且可以用macro-function
&c获得的功能)的函数,时间。这意味着宏的所有扩展成本都是程序的编译时成本的一部分,因此,如果程序多次运行,宏的扩展成本将逐渐变为零。宏的运行时成本是评估返回的代码的成本,因为编译后的代码中没有宏:编译器将它们全部扩展了,只在代码中扩展了它们。 从这一点很明显,首先,函数和宏在程序中扮演着本质上不同的角色–函数执行运行时计算,而宏允许您扩展语言–其次,宏的运行时成本为零。
事情可能比这复杂的有两个原因。
第一个是,很早以前在Lisp的历史中,人们就想优化小型函数的方法:那些调用函数的固定开销足够大的函数。 Lisp编译器是原始的东西,它们不提供执行此操作的功能,并且不够聪明,无法自行执行。而且,当然,事实证明,您可以使用宏来执行此操作:您可以滥用宏提供的功能来计算源代码转换以实现内联函数。人们做到了。
但是那是很久以前的事:Common Lisp提供了两种功能,可以消除对此的需要。
(declaim (inline ...))
即可,任何合理的编译器都会为您进行内联。此外,现代的Lisp编译器比旧的编译器聪明得多(现在没有人认为编译Lisp如此困难,我们需要特殊的智能硬件,因此我们可以坚持使用笨拙的编译器),并且它们通常会做优化简单调用本身的工作非常出色,尤其是对CL标准中定义的函数本身。
事情可能更复杂的第二个原因是运行时和编译时可能并不总是截然不同的。例如,如果您正在编写一个程序,该程序可以编写程序(除了编写宏,这只是简单的例子),那么事件序列会变得很混乱(例如,compile-run-read-metacompile-compile-run) 。在这种情况下,宏扩展可能会在不同的时间发生,并且您可能最终会遇到与元编译过程相关联的您自己的元宏系统。这超出了此答案的范围。
答案 2 :(得分:2)
在Lisp中,需要进行代码转换。例如,可以使用它实现新的控件结构。
想象一下,我们想在 if 中交换if
子句:
(defmacro nif (test else then)
`(if ,test ,then ,else))
提供这些转换的最初尝试之一是所谓的FEXPR函数:对其参数进行不评估的函数。然后,FEXPR函数可以决定要使用参数的内容以及在哪种情况下要评估的内容。
当使用Lisp解释器时,这可以正常工作-一个直接解释Lisp代码的评估器。但是尚不清楚如何编译此类代码。
可以有效地编译OTOH代码中的宏用法:
因此我们使用宏(而不是FEXPR)的原因之一是它们在编译时就得到了扩展,并且在运行时没有开销。
对于Lisp解释器,该宏将在运行时扩展。
“运行”功能要比宏快,因为“运行宏”还涉及代码扩展
仅在运行时扩展宏。但是在已编译的代码中情况并非如此。
答案 3 :(得分:1)
感谢Scott Hunter指出。
宏仅扩展一次-可以通过
进行检查(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