【问题标题】:how do I pass a list to a common lisp macro?如何将列表传递给常见的 lisp 宏?
【发布时间】:2021-05-10 22:53:27
【问题描述】:

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

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

【问题讨论】:

  • 查看 Common Lisp 变量 CALL-ARGUMENTS-LIMIT 以获得函数的最大可能参数数。
  • 在某些实现中,CALL-ARGUMENTS-LIMIT 的值可以低至 50。另请参阅要使用的函数 REDUCE。
  • 您尝试在宏中计算 args 的长度。这不起作用,因为 ARGs 列表是代码,而不是计算列表。如果您实际上是在宏扩展时计算列表,那么您也可以在宏扩展时完成其余的工作,而 AVG2 宏只需将结果编号插入代码中。因此宏调用在编译代码中基本上没有运行时。
  • 比较宏和函数的性能是没有意义的。
  • 将函数的性能与宏的性能进行比较是有意义的,因为宏可以在编译时进行大量计算。所以在这个例子中(length args)是在函数的情况下程序运行时评估的,而在宏的情况下是在编译时处理的。这使它更有效率。我想知道宏的效率有多高。

标签: common-lisp sbcl


【解决方案1】:

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

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

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

> (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 宏。

所以,让我们为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 ...) 可以工作;
  • 没有损坏;
  • 使用了不可怕的算法;
  • 将在可能的情况下内联该语言的任何有效实现(我敢打赌现在是“所有实现”);
  • 将检测(一些?)可以完全编译掉并替换为编译时常量的情况。

【讨论】:

    【解决方案2】:

    由于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
    

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

    【讨论】:

      【解决方案3】:

      我不认为你真的想要一个包含 100,000 个项目的列表。如果有这么多的骗局,那将有糟糕的表现。您应该考虑使用向量,例如

      (avg2 #(2 3 4))
      

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

      在超向量上尝试reduce

      (reduce #'+ super-vector)
      

      【讨论】:

      • 我认为这是一个离题的答案。我想比较函数和宏的性能,而不是如何有效地处理 100000 个项目。如果您尝试将“超级列表”传递给宏,则它不起作用,因为宏不能那样工作。因此我的问题。
      猜你喜欢
      • 2019-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-24
      • 2013-07-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多