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

问题描述

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

编辑:我为什么要比较两者? 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

解决方法

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

其次,只有当宏与函数等效时,将函数的性能与宏的展开进行比较才有意义。换句话说,这种比较唯一有用的地方是宏被用作内联函数的一种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)))

所以现在它“有效”了。除非它没有:它不安全。看看这个:

> (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 年代风格的我真正想要的是一个内联函数宏。

那么,让我们为 avgavg2 编写一个测试工具。每次更改 av2 时,您都需要重新编译 avg2,实际上您还需要重新编译 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))))

现在您必须确保重新编译 avgav1。当我查看 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 ...) 会起作用;
  • 没有坏;
  • 使用非可怕的算法;
  • 将在可能的情况下内联该语言的任何有效实现(我敢打赌现在是“所有实现”);
  • 将检测(某些?)可以完全编译并替换为编译时常量的情况。
,

因为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

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

,

我不认为您真的想要一个包含 100,000 个项目的列表。考虑到所有这些,那将有糟糕的表现。你应该考虑一个向量,例如

(avg2 #(2 3 4))

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

在超级向量上尝试 reduce

(reduce #'+ super-vector)