警告:本文含有 Lisp 黑科技,请小心阅读。
很多时候配置文件里面有类似这样的代码:
(defconst spacemacs-core-directory
(expand-file-name (concat spacemacs-start-directory "core/"))
事实上,配置文件在安装后路径基本不会变,实际路径一直都会是 "~/.emacs.d/core/"
,每次启动 Emacs 以后都要重新调用 concat
和 spacemacs-start-directory
。这虽然只是很小的性能损失,但是积少成多,尤其是对于大型配置文件。而且除了路径,还有许多类似的地方可以优化。
比如,我们需要在函数中使用一个 (1 2 ... 100)
的列表,每次用 dotimes
或者 mapc
建立列表定然是不如用直接用一个写好了的列表快。但是我们会蠢到为了一点运行效率手写 1 到 100?不可能的。
不像 Common Lisp 有非常强大的编译器优化功能,Emacs Lisp 的字节码编译器优化程度不高,有很大提升空间。其中一个方式就是用宏。
以下是通过“读入”宏(Emacs Lisp 实际上没有读入宏)把这些计算负担分到编译字节码阶段的办法。代码参考了 backquote.el
。
;;; -*- lexical-binding: t -*-
;;;###autoload
(defmacro exclamation (structure)
"Expand the arguments to reduce loading time."
(exclamation-process structure))
;;;###autoload
(defalias 'excl 'exclamation)
(defconst exclamation-macro-mark '\$
"Symbol used to represent a macro in exclamation.")
(defun exclamation-process (s)
"Process to expand Sexp of `exclamation'."
(if (listp s)
(loop for i in s
collect (if (and (listp i)
(eq (car i) exclamation-macro-mark))
(eval (cdr i)); 一般来说在宏里面是不能用 `eval' 的,不过开了 lexical-binding 以后问题就不大了。
(exclamation-process i)))
s))
虽然很明显这段代码效率不算高(为了代码简单用了不适合 Emacs Lisp 的尾调递归),而且比较简陋,但是由于只要在编译的时候调用,宏展开完成后反过来能提高字节码的效率。
以下是例子:
(setq foo (concat "fii" "zz"))
生成的字节码:
(byte-code "\301\302\207" [foo "fiizz" nil] 1)
很明显。Emacs Lisp 编译器这点程度的优化能力还是有的。
但是如果变成上面用的例子:
(defconst spacemacs-core-directory (byte-code "\301\302P!\207" [spacemacs-start-directory expand-file-name "core/"] 3))
就不能做优化了,虽然我们知道 spacemacs-start-directory
在运行中基本不会变,但是 Emacs 可不知道。
如果用我的 excl
宏:
(excl (defconst spacemacs-core-directory
($ expand-file-name (concat spacemacs-start-directory "core/"))))
;; 字节码直接就是
(defconst spacemacs-core-directory "/Users/ldbeth/.emacs.d/core/")
明显可以提高生成字节码的效率。
结束语:例子写的很蹩脚,请见谅。
非常不建议在正式发布的 Emacs Lisp Package 中使用这种黑科技,宏会给开发带来严重困扰。尤其我给出的代码潜在问题很大。但是你可以在自己用的配置中实验这种黑科技。
我认为比较理想的方案是把这个 in-line macro的功能植入 bytecomp.el。但是由于 Emacs 加载自身 Lisp 是直接用内存映像,是没有必要实现这个功能的。
另外我充满黑科技的 Emacs 配置文件 inferno 正在火热开发中,期待体验地狱业火般的 Emacs Lisp 编程吧!!!