【问题标题】:In Common Lisp, Is there a performance difference between functions and macros?在 Common Lisp 中,函数和宏之间是否存在性能差异?
【发布时间】:2019-06-05 12:20:39
【问题描述】:

考虑以下两个定义:

(defun fun-add (a b) (+ a b))
(defmacro macro-add (a b) `(+ ,a ,b))

在我有限的理解中,“运行”函数会比宏快,因为“运行宏”还涉及代码扩展。但是,我使用 SBCL 得到以下结果:

CL-USER> (time (loop for i below 1e7
                     do (fun-add 15 25)))
Evaluation took:
  0.180 seconds of real time
  0.179491 seconds of total run time (0.179491 user, 0.000000 system)
  99.44% CPU
  396,303,718 processor cycles
  0 bytes consed

NIL


CL-USER> (time (loop for i below 1e7
                     do (macro-add 15 25)))
Evaluation took:
  0.034 seconds of real time
  0.033719 seconds of total run time (0.033719 user, 0.000000 system)
  100.00% CPU
  74,441,518 processor cycles
  0 bytes consed

NIL

为什么会这样?

【问题讨论】:

  • 宏不是只需要展开一次吗?
  • 嗯...是的,只是通过维护一个计数器来检查。宏确实只扩展了一次。有没有办法让它扩大多次?
  • 有点违背宏的目的,不是吗?
  • @ScottHunter,也许吧。我想根据参数的类型来衡量使用宏来扩展代码的性能开销。 (此信息将在编译时通过另一个参数获得。)有没有办法衡量宏的性能?
  • 我不明白你在测量什么;您说“使用宏的性能开销”,但与什么相比的开销?如果您想在一个参数已知的情况下打开一些调用代码,请使用例如编译器宏,或者如果你想更深入,使用deftransformstackoverflow.com/questions/44342104/…

标签: performance lisp common-lisp


【解决方案1】:

有没有办法让它扩大多次?

事实上,是的。

这是一个示例,首先是使用宏时通常预期的情况,即宏在评估之前仅被扩展一次:

; SLIME 2.23
CL-USER> (defmacro test () (print "EXPANDING"))
TEST
CL-USER> (test)

"EXPANDING" ;; printed
"EXPANDING" ;; return value

CL-USER> (dotimes (i 10) (test))

"EXPANDING" 
NIL

现在,切换到解释模式:

CL-USER> (setf sb-ext:*evaluator-mode* :interpret)
:INTERPRET

CL-USER> (dotimes (i 10) (test))

"EXPANDING"
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 
"EXPANDING" 

解释模式可能很有用。如果您想开发一个宏并且不想在每次更新代码时重新编译所有调用者,则使用宏。

然而,性能会受到惩罚,所以我认为基准测试并不相关。此外,您最初提出的问题是将苹果与橙子进行比较,因为宏的用途与函数的用途完全不同。

【讨论】:

  • 您能否详细说明“您不想在每次更新代码时重新编译所有调用者”?你指的是调用宏的函数吗?
  • 例如:编写使用Alexandriaif-let宏的函数A;评估 defun 以便函数被编译/已知(不在解释模式下)。然后,将alexandria:if-let 重新定义为不同的东西(例如,在宏扩展期间抛出错误)。您的原始函数 A 仍然有效,宏在 A 编译时展开。只有当您尝试重新编译 A 时,您才会有更改宏定义的效果。这与内联函数完全一样。
【解决方案2】:

这个问题暴露了一些困惑,我认为值得尝试解决这种困惑的答案。

首先,宏和函数在 Lisp 代码中的作用不同,如果您想知道在特定情况下使用哪个,您几乎肯定会犯错误。

  • 函数(也许更正确地称为过程,因为它们可能不计算函数)是进行运行时计算的东西:它们有参数,返回结果并且可能有边-效果。而且它们有一些运行时成本,包括调用它们的可能的固定开销。有一些技巧可以降低固定成本,还有一些技巧可以让您检测和优化特殊情况:见下文。它们也有一些编译时成本:编译器不是即时的。函数的编译时成本通常可以在运行时对它的很多调用进行分摊,并被视为渐近为零。这并不总是正确的:例如,在交互式环境中开发程序时,您可能非常关心编译时间成本。
  • 是一些函数,它们以一些源代码 作为参数并计算另一部分源代码:它们的扩展。扩展宏的函数(defmacro 定义的东西,你可以用macro-function &c 得到)在编译时调用,而不是在运行时调用。这意味着宏的所有扩展成本都是程序编译时成本的一部分,因此,如果程序运行多次,宏的扩展成本会渐近为零。宏的运行时成本是评估它返回的代码的成本,因为编译后的代码中没有宏:它们都被编译器扩展了,只在代码中留下了它们的扩展。

从这一点很清楚,首先,函数和宏在程序中扮演着本质上不同的角色——函数进行运行时计算,而宏让你扩展语言——其次,宏的运行时成本为零。

事情可能比这更复杂的原因有两个。

首先,在很久以前的 Lisp 史前,人们想要优化小函数的方法:调用函数的固定开销足够大的函数。 Lisp 编译器是原始的东西,没有提供这样做的工具,也不够聪明,不能自己做。而且,当然,事实证明您可以使用宏来做到这一点:您可以滥用宏为您提供的工具来计算源代码转换以实现内联函数。人们这样做了。

但那是很久以前的事了:Common Lisp 提供了两个工具来消除对这个的需求。

  • 您可以将函数声明为 inline,这会向编译器发出一个巨大的提示,即它应该对它们进行内联调用。并且您可以在完全不改变函数定义的情况下做到这一点:您只需在代码中添加合适的(declaim (inline ...))s,任何合理的编译器都会为您内联。
  • 您可以定义编译器宏,这是一种特殊类型的宏,与编译器将在编译时调用的函数相关联,例如,允许检测特别简单的调用函数并优化它们,同时处理更复杂的调用。同样,编译器宏根本不会干扰正常的函数定义,尽管它们应该小心扩展为与它们作为编译器宏的函数等效的代码。

除此之外,现代 Lisp 编译器比老式编译器聪明得多(现在没有人认为编译 Lisp 如此困难,我们需要特殊的智能硬件,这样我们才能坚持使用愚蠢的编译器),而且它们通常会做在优化简单调用方面做得非常好,尤其是对 CL 标准中定义的函数本身。

事情可能更复杂的第二个原因是运行时和编译时可能并不总是不同的。例如,如果您正在编写一个编写程序的程序(不仅仅是编写宏,这是简单的情况),那么事件序列可能会变得非常复杂(例如 compile-run-read-metacompile-compile-run) .在这种情况下,宏扩展可能发生在不同的时间,您最终可能会得到与元编译过程相关联的基本上自己的元宏系统。这超出了这个答案的范围。

【讨论】:

    【解决方案3】:

    在 Lisp 中,需要进行代码转换。例如,可以用它实现新的控制结构。

    假设我们想在 not if 中交换 if 子句:

    (defmacro nif (test else then)
      `(if ,test ,then ,else))
    

    提供这些转换的最初尝试之一是所谓的 FEXPR 函数:其参数未经计算的函数。然后,FEXPR 函数可以决定使用哪些参数以及在哪些情况下评估哪些参数。

    这在使用 Lisp 解释器时可以正常工作 - 一个直接解释 Lisp 代码的评估器。但不清楚如何编译这样的代码。

    OTOH代码中的宏用法可以高效编译:

    • 代码被扩展
    • 扩展的代码被编译

    因此,我们使用宏(而不是 FEXPR)的原因之一是它们在编译时得到扩展,并且在运行时没有开销。

    对于 Lisp 解释器,宏将在运行时展开。

    “运行”函数会比宏快,因为“运行宏”还涉及代码扩展

    仅当宏要在运行时扩展时。但在编译后的代码中,情况并非如此。

    【讨论】:

      【解决方案4】:

      感谢Scott Hunter 指出。

      宏只展开一次 - 可以通过以下方式检查

      (defvar *macro-count* 0)
      (defmacro macro-add (a b) 
        (incf *macro-count*)
        `(+ ,a ,b))
      
      CL-USER> (time (loop for i below 1e8
                           do (macro-add 15 25)))
      Evaluation took:
        0.335 seconds of real time
        0.335509 seconds of total run time (0.335509 user, 0.000000 system)
        100.30% CPU
        740,823,874 processor cycles
        0 bytes consed
      
      NIL
      CL-USER> *macro-count*
      1
      

      【讨论】:

      • 扩展数为一 - 在此评估模式下的此实现中 - 其他实现可能表现不同,并可能显示不同数量的宏扩展
      • (+ a b) 应该是(+ ,a ,b) ?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-03-05
      • 2012-12-01
      • 2014-05-17
      • 2014-02-23
      • 2010-10-02
      • 2012-08-21
      • 1970-01-01
      相关资源
      最近更新 更多