EMACS-DOCUMENT

=============>集思广益

聊一聊Emacs的字节编译

某些Lisp有一项独门绝迹,那就是可以在任意时刻将函数编译成字节码/本地代码(具体编译成什么有赖于方言和具体实现). 对于那些能够编译的Lisp函数(例如CLISP), 一般都支持两种形式的代码: 一种是缓慢的,未优化未编译过的代码,一种是快速,高效编译过的代码. 但是你不要以为未编译的代码就一无是处了,除了无需花费时间在编译上之外,它也有其他的有点.

对于Emacs Lisp来说, 未编译过的函数就是一个lambda表达式. 只不过它存在于一个symbol中,就相当与给它取了个名字. 而编译后的form则是一个(特殊的)数组, 编译后的字节码以字符串的形式存在这个数组的第二个元素中. 其他的元素还包括常量,docstring等. Elisp提供了 byte-compile 函数来编译其他函数. 该函数能接受一个lambda函数或者一个symbol. 不过如果参数是symbol的话,编译后的函数回覆盖该symbol的原S表达式.

(byte-compile (lambda (x) (* 2 x)))
=> #[(x) "^H\301_\207" [x 2] 2]

编译器不仅仅是将函数是转换成字节码并展开宏,还会进行一些优化,比如删除无用代码,预先执行 safe constant forms, 以及内联函数. 从而极大地提高执行效率. (通过我的 measure-time macro 来测算的)

(defun fib (n)
  "Fibonacci sequence."
  (if (<= n 2) 1
    (+ (fib (- n 1)) (fib (- n 2)))))

(measure-time
 (fib 30))
=> 1.0508708953857422

(byte-compile 'fib)

(measure-time
 (fib 30))
=> 0.4302399158477783

大多数Emacs自带的函数都预先编译过了. 不过也有部分自带的函数没有被编译,所以我想,不如我来花点时间把它们编译一下算了.

Common Lisp是有提供一个判断函数(compiled-function-p)来测试另一个函数是否有编译过的. 但不知为何,Elisp中没有预定义这样一个函数,我只能自己写一个了:

(defun byte-compiled-p (func)
  "Return t if function is byte compiled."
  (cond
   ((symbolp   func) (byte-compiled-p (symbol-function func)))
   ((functionp func) (not (sequencep func)))
   (t nil)))

我一开始的想法是便利所有interned symbol,若通过上面的测试函数发现该symbol的function slot中是未编译的函数,那么就调用 byte-compile 来编译它. 不过后来我发现, byte-compile 足够聪明,它会自动忽略那些没有函数以及那些函数以及编译过的symbol.

那么,下一个问题就是,我们如何遍历每个interned symbol? 函数 mapatoms 可以做到这一点. 给它一个函数作为参数,它就会将该函数应用到每个interned symbol上. 这样一来,问题就简单了. (译者注:我运行了这个语句之后,Evil彻底用不了了,不得不重启Emacs...)

(mapatoms 'byte-compile)

这就搞定了! 整个过程不过几秒钟,不过会产生大量的警告. 目前我还未找到屏蔽这些警告方法,所以你最好不要自动运行这个语句,否则你会发现时不时地会弹出一个window. 而且,我也不确定这么做会不会损坏你的Emacs session,毕竟不是所有的函数写的时候都有好好的考虑编译后还能不能用,尤其是那些用到宏的函数.

另外,我很怀疑这样能否带来性能上的明显提升. 就像我前面所得,大多数的函数预先编译过了,而这部分函数才是使用最频繁的函数. 把所有函数都编译一次,可能只是给你你心理上安慰一点而已.