Performance 在Common Lisp中,函数和宏之间是否存在性能差异?

Performance 在Common Lisp中,函数和宏之间是否存在性能差异?,performance,lisp,common-lisp,Performance,Lisp,Common Lisp,考虑以下两个定义: defun fun添加一个b+a b defmacro宏添加一个b`+,a,b 在我有限的理解中,运行函数要比运行宏快,因为运行宏还涉及代码扩展。但是,我通过SBCL得到以下结果: CL-USER>1e7以下i的时间循环 玩得开心加15 25 评价采取了: 0.180秒的实时性 总运行时间0.179491秒0.179491用户,0.000000系统 99.44%CPU 396303718处理器周期 消耗0字节 无 CL-USER>1e7以下i的时间循环 宏加15和25吗 评价

考虑以下两个定义:

defun fun添加一个b+a b defmacro宏添加一个b`+,a,b 在我有限的理解中,运行函数要比运行宏快,因为运行宏还涉及代码扩展。但是,我通过SBCL得到以下结果:

CL-USER>1e7以下i的时间循环 玩得开心加15 25 评价采取了: 0.180秒的实时性 总运行时间0.179491秒0.179491用户,0.000000系统 99.44%CPU 396303718处理器周期 消耗0字节 无 CL-USER>1e7以下i的时间循环 宏加15和25吗 评价采取了: 0.034秒的实时性 总运行时间0.033719秒0.033719用户,0.000000系统 100.00%CPU 74441518处理器周期 消耗0字节 无 为什么会这样

谢谢你的指点

宏只展开一次-这可以通过

defvar*宏计数*0 defmacro宏添加a b incf*宏计数* `+,a,b CL-USER>1e8以下i的时间循环 宏加15和25吗 评价采取了: 0.335秒的实时性 总运行时间0.335509秒0.335509用户,0.000000系统 100.30%CPU 740823874处理器周期 消耗0字节 无 CL-USER>*宏计数* 1. 谢谢你的指点

宏只展开一次-这可以通过

defvar*宏计数*0 defmacro宏添加a b incf*宏计数* `+,a,b CL-USER>1e8以下i的时间循环 宏加15和25吗 评价采取了: 0.335秒的实时性 总运行时间0.335509秒0.335509用户,0.000000系统 100.30%CPU 740823874处理器周期 消耗0字节 无 CL-USER>*宏计数* 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
现在,切换到解释模式:

如果您想要开发宏,并且不想在每次更新代码时重新编译所有调用方,那么解释模式对于w.r.t.宏非常有用

然而,有一个表现会受到惩罚,所以我不认为基准将是相关的。此外,您最初提出的问题是比较苹果和橙子,因为宏的用途与函数的用途大不相同

有没有办法让它扩展多次

事实上,是的

下面是一个示例,首先是使用宏时通常会遇到的情况,即在求值之前仅展开一次宏:

; 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
现在,切换到解释模式:

如果您想要开发宏,并且不想在每次更新代码时重新编译所有调用方,那么解释模式对于w.r.t.宏非常有用


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

这个问题暴露出一些困惑,我认为有必要回答这个问题,试图解决这个困惑

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

函数可能更准确地称为过程,因为它们可能不计算运行时计算的函数:它们有参数,返回结果,可能有副作用。而且,它们还有一些运行时成本,包括可能的固定费用。有一些技巧可以降低固定成本,也有一些技巧可以让您检测和优化特殊情况:见下文。它们也有一些编译时成本:编译器不是即时的。一个函数的编译时成本通常可以在运行时对它进行多次调用,并被视为渐近零。这并不总是正确的:例如,在交互式环境中开发程序时,您可能非常关心编译时成本。 宏是以一点源代码作为参数并计算另一点源代码的函数:它们的扩展。对宏进行扩展的函数是defmacro定义的,您可以使用宏函数&c在编译时调用,而不是在运行时调用。这意味着宏的所有扩展成本都是程序编译时成本的一部分,因此,如果程序运行多次,宏的扩展成本将渐近为零。宏的运行时成本是计算它返回的代码的成本,因为编译代码中没有宏:编译器已对它们进行了扩展,只在代码中保留了它们的扩展。 很清楚 首先,函数和宏在程序中扮演着本质上不同的角色——函数执行运行时计算,而宏允许您扩展语言——其次,宏的运行时成本为零

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

第一个是,很久以前在Lisp的史前时期,人们想要优化小函数的方法:调用函数的固定开销足够大的函数。Lisp编译器是一些原始的东西,它们没有提供实现这一点的工具,也没有足够的智能来自行实现这一点。当然,事实证明你可以用宏来实现这一点:你可以滥用宏提供给你的工具来计算源代码转换来实现内联函数。人们做到了这一点

但那是很久以前的事了:CommonLisp提供了两种工具,消除了这种需要

您可以将函数声明为内联函数,这会向编译器发出一个巨大的提示,提示它应该内联调用这些函数。您可以做到这一点,而无需修改函数的定义:只需在代码中添加合适的declaim inline…s,任何合理的编译器都会为您进行内联。 您可以定义编译器宏,它是一种特殊类型的宏,与编译器将在编译时调用的函数关联,例如,允许它检测函数的特别简单调用并对其进行优化,同时进行更复杂的调用。同样,编译器宏根本不会干扰正常的函数定义,尽管它们应该小心地扩展到与它们作为编译器宏的函数等效的代码。 除此之外,现代的Lisp编译器比老式的要聪明得多——现在没有人认为编译Lisp太难了,我们需要特殊的智能硬件,这样我们就可以使用哑编译器,而且它们通常在优化简单调用方面做得很好,特别是对CL标准中定义的函数本身


事情可能更复杂的第二个原因是运行时和编译时可能并不总是不同的。例如,如果您正在编写一个程序,该程序编写的程序不仅仅是编写宏,这是一个简单的例子,那么事件序列可能会变得非常复杂,例如编译运行读取元编译运行。在这种情况下,宏扩展可以在不同的时间进行,最终您可能会得到与元编译过程相关联的元宏系统。这超出了这个答案的范围。

这个问题暴露出一些困惑,我认为值得一个试图解决这个困惑的答案

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

函数可能更准确地称为过程,因为它们可能不计算运行时计算的函数:它们有参数,返回结果,可能有副作用。而且,它们还有一些运行时成本,包括可能的固定费用。有一些技巧可以降低固定成本,也有一些技巧可以让您检测和优化特殊情况:见下文。它们也有一些编译时成本:编译器不是即时的。一个函数的编译时成本通常可以在运行时对它进行多次调用,并被视为渐近零。这并不总是正确的:例如,在交互式环境中开发程序时,您可能非常关心编译时成本。 宏是以一点源代码作为参数并计算另一点源代码的函数:它们的扩展。对宏进行扩展的函数是defmacro定义的,您可以使用宏函数&c在编译时调用,而不是在运行时调用。这意味着宏的所有扩展成本都是程序编译时成本的一部分,因此,如果程序运行多次,宏的扩展成本将渐近为零。宏的运行时成本是计算它返回的代码的成本,因为编译代码中没有宏:编译器已对它们进行了扩展,只在代码中保留了它们的扩展。 从这一点可以非常清楚地看出,首先,函数和宏在程序中扮演着本质上不同的角色——函数执行运行时计算,而宏允许您扩展语言——其次,宏的运行时成本为零

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

第一个是,很久以前在Lisp的史前时期,人们想要优化小函数的方法:调用函数的固定开销足够大的函数。Lisp编译器是最原始的东西,而不是 提供设施来做这件事,但他们自己不够聪明。当然,事实证明你可以用宏来实现这一点:你可以滥用宏提供给你的工具来计算源代码转换来实现内联函数。人们做到了这一点

但那是很久以前的事了:CommonLisp提供了两种工具,消除了这种需要

您可以将函数声明为内联函数,这会向编译器发出一个巨大的提示,提示它应该内联调用这些函数。您可以做到这一点,而无需修改函数的定义:只需在代码中添加合适的declaim inline…s,任何合理的编译器都会为您进行内联。 您可以定义编译器宏,它是一种特殊类型的宏,与编译器将在编译时调用的函数关联,例如,允许它检测函数的特别简单调用并对其进行优化,同时进行更复杂的调用。同样,编译器宏根本不会干扰正常的函数定义,尽管它们应该小心地扩展到与它们作为编译器宏的函数等效的代码。 除此之外,现代的Lisp编译器比老式的要聪明得多——现在没有人认为编译Lisp太难了,我们需要特殊的智能硬件,这样我们就可以使用哑编译器,而且它们通常在优化简单调用方面做得很好,特别是对CL标准中定义的函数本身


事情可能更复杂的第二个原因是运行时和编译时可能并不总是不同的。例如,如果您正在编写一个程序,该程序编写的程序不仅仅是编写宏,这是一个简单的例子,那么事件序列可能会变得非常复杂,例如编译运行读取元编译运行。在这种情况下,宏扩展可以在不同的时间进行,最终您可能会得到与元编译过程相关联的元宏系统。这超出了这个答案的范围。

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

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

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

当使用Lisp解释器(一种直接解释Lisp代码的求值器)时,这种方法可以正常工作。但目前还不清楚如何编译这样的代码

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

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

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

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


仅当宏要在运行时展开时。但在编译代码中并非如此。

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

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

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

当使用Lisp解释器(一种直接解释Lisp代码的求值器)时,这种方法可以正常工作。但目前还不清楚如何编译这样的代码

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

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

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

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


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

宏不需要只扩展一次吗?嗯。。。是的,只是通过维护一个计数器来检查。宏实际上只扩展了一次。有没有办法让它扩展多次?有点违背了宏的目的,不是吗?@ScottHunter,也许。我想根据参数的类型来衡量使用宏扩展代码的性能开销。在编译时,可以通过另一个参数获得此信息。有没有办法衡量宏的性能?我不明白你在衡量什么;你说使用宏的性能开销,但与什么相比呢?如果你想打开
-当一个参数已知时,编写一些调用代码,例如使用编译器宏,或者如果您想更深入,请使用deftransform:宏不需要只展开一次吗?嗯。。。是的,只是通过维护一个计数器来检查。宏实际上只扩展了一次。有没有办法让它扩展多次?有点违背了宏的目的,不是吗?@ScottHunter,也许。我想根据参数的类型来衡量使用宏扩展代码的性能开销。在编译时,可以通过另一个参数获得此信息。有没有办法衡量宏的性能?我不明白你在衡量什么;你说使用宏的性能开销,但与什么相比呢?如果您想在已知一个参数时打开一些调用的代码,请使用编译器宏,或者如果您想更深入,请使用deftransform:请详细说明您不想在每次更新代码时重新编译所有调用方,好吗?您是指调用宏的函数吗?例如:编写使用s if let宏的函数A;对defun求值,使函数在解释模式下编译/已知。然后,重新定义alexandria:如果让它成为不同的东西,例如在宏扩展期间抛出错误。您的原始函数A仍然有效,宏在编译A时已展开。只有当您尝试重新编译宏时,才会产生更改宏定义的效果。这与内联函数完全相同。请详细说明您不想在每次更新代码时重新编译所有调用方吗?您是指调用宏的函数吗?例如:编写使用s if let宏的函数A;对defun求值,使函数在解释模式下编译/已知。然后,重新定义alexandria:如果让它成为不同的东西,例如在宏扩展期间抛出错误。您的原始函数A仍然有效,宏在编译A时已展开。只有当您尝试重新编译宏时,才会产生更改宏定义的效果。这与内联函数完全相同。扩展的数量是一个-在此评估模式下的实现中-其他实现的行为可能不同,并且可能显示不同数量的宏扩展+a b应该是+,a,b?扩展的数量是一个-在此评估模式下的实现中-其他实现可能表现不同,并且可能显示不同数量的宏扩展+a b应该是+,a,b?
(defmacro nif (test else then)
  `(if ,test ,then ,else))