首先,“比较函数和宏的性能”根本没有意义。只有将宏的扩展 的性能与函数进行比较才有意义。所以这就是我要做的。
其次,只有在宏等效于函数的情况下,将函数的性能与宏的扩展进行比较才有意义。换句话说,这种比较唯一有用的地方是宏被用作内联函数的一种hacky方式。比较函数无法表达的东西的性能是没有意义的,比如if 或and 说。所以我们必须排除宏的所有有趣用途。
第三,比较被破坏的东西的性能是没有意义的:很容易让不工作的程序随心所欲地运行。所以我会依次修改你的函数和宏,以免它们被破坏。
第四,比较使用非常糟糕的算法的事物的性能是没有意义的,所以我将修改你的函数和你的宏以使用更好的算法。
最后,如果不使用语言提供的鼓励良好性能的工具来比较事物的性能是没有意义的,所以我将把它作为最后一步。
所以让我们来解决上面的第三点:让我们看看avg(以及因此avg2)是如何被破坏的。
这是问题中avg 的错误定义:
(defun avg (args)
(/ (apply #'+ args) (length args)))
那么让我们试试吧:
> (let ((l (make-list 1000000 :initial-element 0)))
(avg l))
Error: Last argument to apply is too long: 1000000
天哪,正如其他人指出的那样。所以可能我需要让avg 至少工作。正如其他人再次指出的那样,这样做的方法是reduce:
(defun avg (args)
(/ (reduce #'+ args) (length args)))
现在至少可以调用avg。 avg 现在没有问题了。
我们还需要使avg2 无故障。好吧,首先(+ ,@args) 是一个非首发:args 是宏扩展时的符号,而不是列表。所以我们可以试试这个(apply #'+ ,args)(宏的扩展现在开始看起来有点像函数体,这不足为奇!)。所以给定
(defmacro avg2 (args)
`(/ (apply #'+ ,args) (length ,args)))
我们得到
> (let ((l (make-list 1000000 :initial-element 0)))
(avg2 l))
Error: Last argument to apply is too long: 1000000
好的,再次不足为奇。让我们修复它以再次使用reduce:
(defmacro avg2 (args)
`(/ (reduce #'+ ,args) (length ,args)))
所以现在它“工作”了。除非它没有:它不安全。看看这个:
> (macroexpand-1 '(avg2 (make-list 1000000 :initial-element 0)))
(/ (reduce #'+ (make-list 1000000 :initial-element 0))
(length (make-list 1000000 :initial-element 0)))
t
这绝对是不对的:它会非常慢,但也只是有问题。我们需要解决多重评估问题。
(defmacro avg2 (args)
`(let ((r ,args))
(/ (reduce #'+ r) (length r))))
这在所有正常情况下都是安全的。所以现在这是一个相当安全的 70 年代风格 what-I-really-want-is-an-inline-function 宏。
所以,让我们为avg 和avg2 编写一个测试工具。每次更改avg2 时都需要重新编译av2,事实上,您还需要重新编译av1 才能对avg 进行更改。还要确保所有内容都已编译!
(defun av0 (l)
l)
(defun av1 (l)
(avg l))
(defun av2 (l)
(avg2 l))
(defun test-avg-avg2 (nelements niters)
;; Return time per call in seconds per iteration per element
(let* ((l (make-list nelements :initial-element 0))
(lo (let ((start (get-internal-real-time)))
(dotimes (i niters (- (get-internal-real-time) start))
(av0 l)))))
(values
(let ((start (get-internal-real-time)))
(dotimes (i niters (float (/ (- (get-internal-real-time) start lo)
internal-time-units-per-second
nelements niters)))
(av1 l)))
(let ((start (get-internal-real-time)))
(dotimes (i niters (float (/ (- (get-internal-real-time) start lo)
internal-time-units-per-second
nelements niters)))
(av2 l))))))
所以现在我们可以测试各种组合了。
好的,现在第四点:avg 和 avg2 都使用糟糕的算法:它们遍历列表两次。好吧,我们可以解决这个问题:
(defun avg (args)
(loop for i in args
for c upfrom 0
summing i into s
finally (return (/ s c))))
类似
(defmacro avg2 (args)
`(loop for i in ,args
for c upfrom 0
summing i into s
finally (return (/ s c))))
这些更改对我来说造成了大约 4 倍的性能差异。
好的,现在最后一点:我们应该使用语言提供的工具。正如在整个练习中所清楚的那样,只有将宏用作穷人的内联函数时才有意义,就像人们在 1970 年代所做的那样。
但现在已经不是 1970 年代了:我们有内联函数。
所以:
(declaim (inline avg))
(defun avg (args)
(loop for i in args
for c upfrom 0
summing i into s
finally (return (/ s c))))
现在您必须确保重新编译avg,然后再编译av1。当我查看 av1 和 av2 时,我现在可以看到它们是相同的代码:avg2 的全部目的现在已经消失了。
事实上,我们可以做得比这更好:
(define-compiler-macro avg (&whole form l &environment e)
;; I can't imagine what other constant forms there might be in this
;; context, but, well, let's be safe
(if (and (constantp l e)
(listp l)
(eql (first l) 'quote))
(avg (second l))
form))
现在我们有一些东西:
- 具有函数的语义,所以说
(funcall #'avg ...) 可以工作;
- 没有损坏;
- 使用了不可怕的算法;
- 将在可能的情况下内联该语言的任何有效实现(我敢打赌现在是“所有实现”);
- 将检测(一些?)可以完全编译掉并替换为编译时常量的情况。