性能控福利:用宏来手动控制并优化编译生成的Emacs Lisp字节码

:warning: 警告:本文含有 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/")

明显可以提高生成字节码的效率。


结束语:例子写的很蹩脚,请见谅。

:warning: :warning: :warning: 非常不建议在正式发布的 Emacs Lisp Package 中使用这种黑科技,宏会给开发带来严重困扰。尤其我给出的代码潜在问题很大。但是你可以在自己用的配置中实验这种黑科技。:warning: :warning: :warning:

我认为比较理想的方案是把这个 in-line macro的功能植入 bytecomp.el。但是由于 Emacs 加载自身 Lisp 是直接用内存映像,是没有必要实现这个功能的。

另外我充满黑科技的 Emacs 配置文件 inferno 正在火热开发中,期待体验地狱业火般的 Emacs Lisp 编程吧!!!

2 个赞

必需十分确定表达式求值结果不变,才能这样写。 而且求值的时机也很难关键,这个宏如何处理需要延迟的情况?(这其实算是求值结果不固定的一种特例吧)。

你的例子当中,编译器不做优化,是因为使用了 expand-file-name 函数,没有人事先知道函数的返回值。这种情况,在其它语言中也未必能优化。

看成“性功能福利”。。。掩面而过

不懂,这个宏和 eval-when-compile 的区别是什么?

是的,所以随意用这个宏是很危险的行为。需要延时的情况就干脆不用这宏了。当然为了方便我也可以扩展成捕捉到变量未定义的时候返回原表达式而不做展开。 但是不用延时求值的情况也挺多。有时候用 (defconst foo (concat val “baz”)) 就是为了方便改动代码,只要修改 val 值就可以了。 在配置文件这种自己用的地方控制比较方便。


附:检查变量是否绑定的版本

(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))
                        (let* ((lst (cdr i))
                               (war
                                (catch 'unbound
                                  (loop for x in lst
                                        do (if (listp x)
                                               (unless (fboundp (car x))
                                                 (throw 'unbound t)
                                                 (warn
                                                  "symbol's function definition void: `%S'"
                                                  (car x)))
                                             (or (not (symbolp x))
                                                 (boundp x)
                                                 (fboundp x)
                                                 (progn
                                                   (throw 'unbound t)
                                                   (warn
                                                    "symbol's definition void: `%S'"
                                                    x))))))))
                          (if war
                              lst
                            (eval lst)))
                      (exclamation-process i)))
    s))

;;; tests
(setq foo 1)
;; => 1

(exclamation-process '(setq foo ($ + 1 foo)))
;; => (setq foo 2)


(exclamation-process '(setq foo ($ + 1 dsadsadas)))
;; Warning (emacs): symbol’s definition void: ‘dsadsadas’
;; => (setq foo (+ 1 dsadsadas))

老污龟…………

eval-when-compile里面的代码是不会编译进字节码的。 这个更像 backquote,不过 back quote 不执行返回的表达式,这个会执行生成的表达式。

或者可以看作类似匿名的 in-line 函数。

我也是。。。。。 :joy: