Common lisp 如何将列表传递给公共lisp宏?

Common lisp 如何将列表传递给公共lisp宏?,common-lisp,sbcl,Common Lisp,Sbcl,我试图比较函数和宏的性能 编辑:为什么我要比较这两者? PaulGraham在他的OnLisp书中写道,宏可以用来提高系统的效率,因为大量的计算可以在编译时完成。因此,在下面的示例中,(length args)在宏情况下的编译时处理,在函数情况下的运行时处理。所以,我只是想知道相对于(avg super list)计算(avg super list)要快多少 以下是函数和宏: (defun avg (args) (/ (apply #'+ args) (length args))) (d

我试图比较函数和宏的性能

编辑:为什么我要比较这两者? PaulGraham在他的OnLisp书中写道,宏可以用来提高系统的效率,因为大量的计算可以在编译时完成。因此,在下面的示例中,
(length args)
在宏情况下的编译时处理,在函数情况下的运行时处理。所以,我只是想知道相对于
(avg super list)
计算
(avg super list)
要快多少

以下是函数和宏:

(defun avg (args)
   (/ (apply #'+ args) (length args)))

(defmacro avg2 (args)
  `(/ (+ ,@args) ,(length args)))

我已经研究了这个问题和其他几个问题,但它们没有帮助,因为它们的解决方案不起作用;例如,在其中一个问题中,用户回答如下:

(avg2 (2 3 4 5))
与此相反:

(avg2 '(2 3 4))

这是可行的,但我想要一个包含100000项的列表:

(defvar super-list (loop for i from 1 to 100000 collect i))

但这不起作用


那么,我如何才能将
超级列表
传递给
avg2

我认为您并不真正想要一个包含100000项的列表。那会有很糟糕的表现。你应该考虑向量,例如

(avg2#(2 3 4))
你没有提到为什么它不起作用;如果函数从未返回,则可能是由于如此大的列表中存在内存问题,或者试图对如此大的函数参数列表应用
;对于可以传递给函数的参数数量,有实现定义的限制

尝试在超级向量上减少

(reduce#'+超向量)

由于超级列表的值已知,因此可以在宏展开时执行所有计算:

(eval-when (:execute :compile-toplevel :load-toplevel)
  (defvar super-list (loop for i from 1 to 100000 collect i)))

(defmacro avg2 (args)
  (setf args (eval args))
  (/ (reduce #'+ args) (length args)))

(defun test ()
  (avg2 super-list))
正在尝试编译的代码:

CL-USER 10 > (time (test))
Timing the evaluation of (TEST)

User time    =        0.000
System time  =        0.000
Elapsed time =        0.000
Allocation   = 0 bytes
0 Page faults
100001/2
因此,运行时接近于零

生成的代码只是一个数字,结果数字:

CL-USER 11 > (macroexpand '(avg2 super-list))
100001/2

因此,对于已知输入,编译代码中的宏调用具有接近零的恒定运行时间。

首先,“比较函数和宏的性能”是没有意义的。比较宏与函数的扩展性能才有意义。这就是我要做的

其次,只有当宏与函数等价时,才有必要将函数的性能与宏的扩展进行比较。换句话说,这种比较唯一有用的地方是宏被用作函数内联的一种黑客方式的地方。比较函数无法表达的东西的性能是没有意义的,比如
if
say。因此,我们必须排除宏的所有有趣用途

第三,比较被破坏的东西的性能是没有意义的:很容易让不起作用的程序以你喜欢的速度运行。因此,我将依次修改函数和宏,使它们不会损坏

第四,比较使用算法的性能是没有意义的,这些算法毫无理由地糟糕,因此我将修改函数和宏以使用更好的算法

最后,如果不使用该语言提供的工具来鼓励良好的性能,那么比较事物的性能是没有意义的,因此我将这样做作为最后一步


因此,让我们讨论上面的第三点:让我们看看
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
了<代码>平均值
现在无故障

我们还需要使
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-a-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倍

最后一点:我们应该使用语言提供的工具。正如在整个练习中所清楚的那样,只有当您使用宏作为穷人的内联函数时才有意义,就像人们在20世纪70年代所做的那样

但现在已经不是20世纪70年代了:我们有内联函数

因此:

现在您必须确保重新编译
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…
    将起作用
  • 他没有破产
    (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))