Elisp同一功能不同实现的宏与函数版本之间性能差别很大


#1

首先,请原谅我用这么一个又长又混乱又不知所云的标题。因为这个问题实在是过于诡异,我无法用简单的标题进行描述。还是看下面的代码吧

(setq lexical-binding t)

;; From dash.el
(defmacro --filter (form list)
  "Anaphoric form of `-filter'.

See also: `--remove'."
  (declare (debug (form form)))
  (let ((r (make-symbol "result")))
    `(let (,r)
       (--each ,list (when ,form (!cons it ,r)))
       (nreverse ,r))))

(defun -filter (pred list)
  "Return a new list of the items in LIST for which PRED returns a non-nil value.

Alias: `-select'

See also: `-keep', `-remove'."
  (--filter (funcall pred it) list))

dash里面有这么两个函数,功能和cl-remove-if-not是一样的。

然后我自己实现了这么一个函数

(defmacro my--filter (form list)
  (let ((elem (make-symbol "elem")))
    `(apply #'nconc (mapcar (lambda (,elem)
                              (and (funcall (lambda (it) ,form) ,elem)
                                   (list ,elem))) ,list))))

(defun my-filter (fn list)
  (my--filter (funcall fn it) list))

功能大致上和--filter是一样的,至少输入参数是列表时行为和--filter一致

接下来有趣的事情要发生了

(-dotimes 10
         (lambda (x)
           (benchmark 1000 '(-filter #'cl-evenp '(3 4 5 6 7)))
           (benchmark 1000 '(my-filter #'cl-evenp '(3 4 5 6 7)))
           (message "\n")))
Elapsed time: 0.001790s
Elapsed time: 0.002837s
 [2 times]
Elapsed time: 0.000748s
Elapsed time: 0.003137s
 [2 times]
Elapsed time: 0.000803s
Elapsed time: 0.003746s
 [2 times]
Elapsed time: 0.000755s
Elapsed time: 0.003723s
 [2 times]
Elapsed time: 0.000753s
Elapsed time: 0.003730s
 [2 times]
Elapsed time: 0.000766s
Elapsed time: 0.003868s
 [2 times]
Elapsed time: 0.000758s
Elapsed time: 0.003937s
 [2 times]
Elapsed time: 0.000836s
Elapsed time: 0.003930s
 [2 times]
Elapsed time: 0.000756s
Elapsed time: 0.003832s
 [2 times]
Elapsed time: 0.000701s
Elapsed time: 0.003863s

(-dotimes 10
         (lambda (x)
           (benchmark 1000 '(--filter (cl-evenp it) '(3 4 5 6 7)))
           (benchmark 1000 '(my--filter (cl-evenp it) '(3 4 5 6 7)))
           (message "\n")))
Elapsed time: 0.006269s
Elapsed time: 0.003397s
 [2 times]
Elapsed time: 0.006261s
Elapsed time: 0.003413s
 [2 times]
Elapsed time: 0.005818s
Elapsed time: 0.049709s (0.045985s in 1 GCs)
 [2 times]
Elapsed time: 0.005700s
Elapsed time: 0.003090s
 [2 times]
Elapsed time: 0.005880s
Elapsed time: 0.003282s
 [2 times]
Elapsed time: 0.006212s
Elapsed time: 0.003704s
 [2 times]
Elapsed time: 0.006333s
Elapsed time: 0.003446s
 [2 times]
Elapsed time: 0.006209s
Elapsed time: 0.003425s
 [2 times]
Elapsed time: 0.005760s
Elapsed time: 0.003607s
 [2 times]
Elapsed time: 0.005901s
Elapsed time: 0.003197s
 [3 times]
 [2 times]

emmm,我自己实现的macro比原装macro快了2倍,但是函数版本比原装函数慢了2倍?不科学啊!

我不是很懂elisp interpreter的内部实现。所以有点好奇,导致宏和函数性能差异巨大的原因究竟是什么?


#2
  1. first of all
 (byte-compile 'my-filter)

my-filter is a Lisp function.

-filter is a compiled Lisp function in ‘dash.el’.

byte-complie 以后差距就不是很大。

Elapsed time: 0.000958s
Elapsed time: 0.001710s
 [2 times]
Elapsed time: 0.000961s
Elapsed time: 0.001710s
  1. 至于第二个。因為 macro 的東西不是这么 benchmark 的,或者说 macro 不是用來 benchmark 的。
(-dotimes 10
  (lambda (x)
    (benchmark 1000 (byte-compile '(--filter (cl-evenp it) '(3 4 5 6 7))))
    (benchmark 1000 (byte-compile '(my--filter (cl-evenp it) '(3 4 5 6 7))))
    (message "\n")))
 [2 times]
Elapsed time: 0.001173s
Elapsed time: 0.001614s
 [2 times]
Elapsed time: 0.001178s
Elapsed time: 0.001588s

至于

这个写法造成 my--filter--filter 快,是因為 my--filter 的 macro expand 比 --filter

(-dotimes 10
  (lambda (x)
    (benchmark 1000 '(macroexp--all-forms '(--filter (cl-evenp it) '(3 4 5 6 7))))
    (benchmark 1000 '(macroexp--all-forms '(my--filter (cl-evenp it) '(3 4 5 6 7))))
    (message "\n")))
Elapsed time: 0.001727s
Elapsed time: 0.001671s
 [2 times]
Elapsed time: 0.001681s
Elapsed time: 0.001711s

macroexpand-all 更明顯。 eval 是用 C 实現的所以做 macro expand 更快。

Elapsed time: 0.016947s
Elapsed time: 0.008644s
 [2 times]
Elapsed time: 0.016758s
Elapsed time: 0.008404s

PS oh sh^t,我又忘记开 lexical binding 了,不过应该对结论没有影响。