如何将列表传递给普通的 lisp 宏?

how do I pass a list to a common lisp macro?

我正在尝试比较函数和宏的性能。

编辑:为什么我要比较两者? Paul Graham 在他的 ON LISP 书中写道,宏可以用来提高系​​统的效率,因为很多计算可以在编译时完成。因此在下面的示例中,(length args) 在宏情况下在编译时处理,在函数情况下在 运行 时间处理。所以,我只想知道 (avg2 super-list) 相对于 (avg super-list).

的计算速度有多快

这是函数和宏:

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

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

我已经查看了这个问题 How to pass a list to macro in common lisp? 和其他一些问题,但他们没有帮助,因为他们的解决方案不起作用;例如,在用户回答的问题之一中说要这样做:

(avg2 (2 3 4 5))

而不是这个:

(avg2 '(2 3 4))

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

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

但这不起作用。

那么,我怎样才能将 super-list 传递给 avg2

我不认为你真的想要一个包含 100,000 个项目的列表。那会带来糟糕的表现。您应该考虑使用矢量,例如

(avg2 #(2 3 4))

你没有提到为什么它不起作用;如果函数从不 returns,则可能是来自如此大的列表的内存问题,或者试图在如此大的函数参数列表上 apply;对于可以传递给函数的参数数量,存在实现定义的限制。

改为在超级向量上尝试 reduce

(reduce #'+ super-vector)

由于super-list的值是已知的,所以可以在宏展开时进行所有计算:

(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

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

首先,'compare the performance of a function and a macro'根本就没有意义。只有将宏的 扩展 的性能与函数进行比较才有意义。所以这就是我要做的。

其次,只有当宏等价于函数时,比较函数的性能和宏的扩展才有意义。换句话说,这种比较唯一有用的地方是宏被用作内联函数的一种 hacky 方式。比较函数无法表达的东西的性能没有意义,比如 ifand 说。所以我们必须排除宏的所有有趣用途。

第三,比较坏掉的东西的性能是没有意义的:很容易让不能运行的程序快到你喜欢的速度。所以我会陆续修改你的函数和宏,这样它们就不会坏了。

第四,比较使用非常糟糕的算法的事物的性能是没有意义的,因此我将修改您的函数和宏以使用更好的算法。

最后,如果不使用语言提供的鼓励良好性能的工具来比较事物的性能是没有意义的,所以我将把它作为最后一步。


所以让我们解决上面的第三点:让我们看看 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)))

现在至少可以调用 avgavg 现在没有错误。

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

所以现在 'works'。除了它没有:它不安全。看看这个:

> (macroexpand-1 '(avg2 (make-list 1000000 :initial-element 0)))
(/ (reduce #'+ (make-list 1000000 :initial-element 0))
   (length (make-list 1000000 :initial-element 0)))
t

这绝对是不对的:它会非常慢,而且还会有 bug。我们需要解决多重评估问题。

(defmacro avg2 (args)
  `(let ((r ,args))
     (/ (reduce #'+ r) (length r))))

这在所有理智的情况下都是安全的。所以现在这是一个相当安全的 70 年代风格的我真正想要的是一个内联函数宏。

那么,让我们为 avgavg2 编写一个测试框架。每次更改 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))))))

现在我们可以测试各种组合了。

好的,现在是第四点:avgavg2 都使用了糟糕的算法:它们遍历列表两次。那么我们可以解决这个问题:

(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。当我查看 av1av2 时,我现在可以看到它们是相同的代码: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 ...) 会起作用;
  • 没坏;
  • 使用了一个不可怕的算法;
  • 将内联到该语言的任何有效实现中(我敢打赌现在是 'all implementations');
  • 将检测(某些?)可以完全编译并替换为编译时常量的情况。