EmacsLisp内置函数的高效别名
假如你不喜欢 car
和 cdr
这两个传统上指代lisp cons cell一半部分的标识符。这两个标识符很有迷惑性。
一个cons实际上就是一个二元组,它的两个半部分本身没有任何特殊的含义,即使认为把他们分成了“头部”和“尾部”。
不过,也许这对你来说确实很重要,所以无论如何你都想折腾一下它。那么最好的折腾方法是什么?
defalias
Emacs Lisp有一个内置的函数, defalias
,这个选择显而易见。
(defalias 'car-alias #'car)
I've followed the definition with its disassembly (Emacs 26.3, lexical scope):
内置的 car
函数对这门语言太过基础,以至于它有自己的操作字节码。在代码中调用 car
时,字节编译器不会生成函数调用,而是使用一条指令。
例如,下面这个 add
函数,它对两个参数的 car
进行求和。我使用了它的反汇编功能来生成定义(Emacs 26.3, 词汇作用域):
(defun add (a b) (+ (car a) (car b))) ;; 0 stack-ref 1 ;; 1 car ;; 2 stack-ref 1 ;; 3 car ;; 4 plus ;; 5 return
由于使用了专用的 car
操作码,所以没有任何函数调用,它生成了最优的6个字节码指令。
defalias
的问题是,由于允许更改定义——或者被 advised——这剥夺了字节编译器的优化机会。这是它的不足之处。
当字节码编译器看到 car-alias
时,它不得不发出一个函数调用:
(defun add-alias (a b) (+ (car-alias a) (car-alias b))) ;; 0 constant car-alias ;; 1 stack-ref 2 ;; 2 call 1 ;; 3 constant car-alias ;; 4 stack-ref 2 ;; 5 call 1 ;; 6 plus ;; 7 return
它有两个函数调用和八个字节码指令。这些函数调用比 car
指令要昂贵得多,这将在稍后的基准测试中展示。
defsubst
另一种方法是 defsubst
,这是一个内联函数定义,它将内联一个实际的 car
.
与宏一样,=defsubst= 的语义也明确表示,重新定义可能不会影响以前的使用,因此不存在上面的不足之处了。
不幸的是Elisp的字节码编译器很笨,而且内联 car-subst
也做得很差。
(defsubst car-subst (x) (car x)) (defun add-subst (a b) (+ (car-subst a) (car-subst b))) ;; 0 stack-ref 1 ;; 1 dup ;; 2 car ;; 3 stack-set 1 ;; 5 stack-ref 1 ;; 6 dup ;; 7 car ;; 8 stack-set 1 ;; 10 plus ;; 11 return
这里有0个函数调用和10个字节码指令。其中直接使用了 car
操作码,但是却多了5个不必要的指令。不过,这仍然比调用函数快。
如果字节码编译器能稍微聪明一点,就可以将其编译为理想情况了,那么讨论就到此为止了。
cl-first
内置的 cl-lib
包有 cl-first
作为 car
的别名。这是一个Emacs Lisp高手写的,那么他们的表现如何呢?
(require 'cl-lib) (defun add-cl-first (a b) (+ (cl-first a) (cl-first b))) ;; 0 stack-ref 1 ;; 1 car ;; 2 stack-ref 1 ;; 3 car ;; 4 plus ;; 5 return
它就像以前的 car
一样!这是怎么做到的?方法是通过使用字节编译器提示:
(defalias 'cl-first 'car) (put 'cl-first 'byte-optimizer 'byte-compile-inline-expand)
他们使用 defalias
,但是同时也手动告诉字节编译器去内联其定义,就好象 defsubst
一样。
事实上 defsubst
本身会扩展为一个设置了 byte-compile-inline-expand
的表达式,但是,正如上面所看到的,内联函数的开销被内联了,并没有消除。
基准测试
那么替代方案的表现如何呢?(基准来源)
add (0.594811299 0 0.0) add-alias (1.232037132 0 0.0) add-subst (0.700044324 0 0.0) add-cl-first (0.58332882 0 0.0)
(列表的 car
操作所花费的运行时间).由于 add
和 add-cl-first
具有相同的字节码,我们不应该也没有看到明显的差异。 简单地使用 defalias
会使运行时间加倍,而使用 defsubst
则会慢15%左右。