emacs性能优化的新进展, 2025版

貌似每年我都有段时间想研究一下windows版本emacs的性能提升, 今年又有新进展
windows上, 200个包, 启动时间0.5秒,
(官方的emacs30.2+chenbin默认配置是1.7秒,而且还有第一次执行某个命令略卡的问题)

提升的思路主要是从 load-path 着手,需要emacs31版本

结论可以看这里 (代码作者 @include-yy )

详细的讨论可以看完整的帖子

7 个赞

Emacs 31 的 load-path-filter-cache-directory-files 用的 try-completion 还是模糊匹配,如果能在把缓存写入文件之前再加个判断过程找到真正的目录应该更快一点,其他部分做点边界处理可以写个包来用用。

先开个坑在这里:

include-yy/persist-cached-load-filter

7 个赞

话说, 为啥缓存里有些没有键, 只有一个路径, 这种貌似就不起作用?

这也是需要改进的,emacs当前的load-path-filter主要对 require起作用,它是有检查加载项的逻辑的,我没加

2 个赞

搓了个 0.1 版本出来:GitHub - include-yy/persistent-cached-load-filter: Persistent cached load filter for Emacs 31+

相比之前的版本精致一些,加了缓存过时判断和退出时若缓存变动则更新缓存文件的逻辑。可以使用 package-vc 安装,照着 README 里面的过程配置就行,配置代码尽量放在 init.el 的头部来缓存尽可能多的包:

(when (require 'persistent-cached-load-filter nil t)
  (persistent-cached-load-filter-easy-setup))

当然我这里还有个邪道配置:

(when (boundp 'load-path-filter-function)
  (setq load-path-filter-function #'load-path-filter-cache-directory-files)
  (when (require 'persistent-cached-load-filter nil t)
    (persistent-cached-load-filter-easy-setup)))

如果你使用 Emacs 31+(目前 (2025-11-19) 还没有发布 30.1 正式版),而且你使用了至少几十个包,这段代码应该能起到一定的启动加速效果。在我的 Emacs 配置中,默认情况下启动用时 4.7~4.9s,使用 load-path-filter-cache-directory-files 启动时间 4.0~4.2s,使用 persistent-cached-load-filter 在完成缓存后启动时间 3.5~3.7s。

(换了个 CPU 好点的机器测了下,分别是 3.0~3.2s,2.6~2.9s,和 2.5~2.7s)

目前使用了 alist 作为缓存数据结构,也许 plist 和 hashtable 效率更高,但对数百个包(或者加载项)差距可能不是太明显。

如果你在使用 Emacs 31,可以尝试用下这个包然后比较默认情况下、使用 Emacs 自带的 load-path-filter-cache-directory-files 下、以及使用我这个包的加载时间。

目前我用着没什么问题,如果使用过程中出现问题/发现了很大的提升,欢迎反馈。

10 个赞

看起来WIndows版Emacs快有救了呀!:+1: 话说这方法也适用于其他平台吧?

1 个赞

应该是的,不过效果可能不会这么显著

写了个测试,从测试结果来看,几百个项对 alist, plist 和 hash-table 来说性能差别不大,而且我的实现减少的时间就来自于对 load-path-filter-cache-directory-files 过滤过程的消除:

;; 把缓存按 alist, plist, hash-table 格式写入测试文件
(with-temp-file "mta.eld"
  (pp (map-into persistent-cached-load-filter--cache 'alist)
      (current-buffer)))
(with-temp-file "mtp.eld"
  (pp (map-into persistent-cached-load-filter--cache 'plist)
      (current-buffer)))
(with-temp-file "mtp.eld"
  (pp (map-into persistent-cached-load-filter--cache 'hash-table)
      (current-buffer)))
;; 测试从测试文件读取得到关联对象的用时
;; 可见对 200 左右长度的关联表时间差距不大
(benchmark-run 1000
  (read (with-temp-buffer
          (insert-file-contents "mta.eld")
          (buffer-string))))
;;0.13ms
(setq map-a (read (with-temp-buffer
                    (insert-file-contents "mta.eld")
                    (buffer-string))))
(benchmark-run 1000
  (read (with-temp-buffer
          (insert-file-contents "mtp.eld")
          (buffer-string))))
;;0.13~0.14ms
(setq map-p (read (with-temp-buffer
                    (insert-file-contents "mtp.eld")
                    (buffer-string))))
(benchmark-run 1000
  (read (with-temp-buffer
          (insert-file-contents "mth.eld")
          (buffer-string))))
;;0.17ms
(setq map-h (read (with-temp-buffer
                    (insert-file-contents "mth.eld")
                    (buffer-string))))
;; 获取关联表的键
(setq keys (map-keys persistent-cached-load-filter--cache))
(length keys)
;;214
;; 测试按照 `persistent-cached-load-filter' 的逻辑查找路径的用时
(benchmark-run 1000
    (mapc (lambda (k)
            (let ((ls (map-elt map-a k)))
              (seq-every-p (lambda (p) (member p load-path)) ls)))
          keys))
;; 1.31ms
(benchmark-run 1000
    (mapc (lambda (k)
            (let ((ls (map-elt map-p k)))
              (seq-every-p (lambda (p) (member p load-path)) ls)))
          keys))
;; 1.09ms
(benchmark-run 1000
    (mapc (lambda (k)
            (let ((ls (map-elt map-h k)))
              (seq-every-p (lambda (p) (member p load-path)) ls)))
          keys))
;; 1.15ms

;; 去掉检查的查找用时
(benchmark-run 1000
  (mapc (lambda (k) (map-elt map-a k)) keys))
;; 0.21ms
(benchmark-run 1000
    (mapc (lambda (k) (map-elt map-p k)) keys))
;; 0.12ms
(benchmark-run 1000
  (mapc (lambda (k) (map-elt map-h k)) keys))
;; 0.034ms
;; `load-path-filter-cache-directory-files' 的参考查找用时
(benchmark-run
    (mapc (lambda (k)
            (load-path-filter-cache-directory-files
             load-path k (get-load-suffixes)))
          keys))
;; 0.1s

这是在我 CPU 比较好(当然只是笔记本 Intel Ultra 7 255H)的机器上测试的,与上面测试数据中的 2.6~2.9s 与 2.5~2.7s 差不多吻合。

CPU 越好带来的提升应该越小 :rofl:

我发现个问题, 之前的代码片段是可以放在 early-init.el里面的, 放在 early-init.el里面要快个0.05秒左右 (虽然没太大区别)
但是用这个包的话, 就只能放在init.el里面了

放early-init.el应该是让emacs找内置el的速度也加快了(不知道是不是个例, 你那边测试呢?)

把整个包放到 early-init.el 里面也不是不行,可以考虑手动添加包的路径到 load-pathrequire、或者是把包放在某个路径下手动 load-file,或者是把整个包丢到 early-init.el 里面(不过这样做得把里面的 t- 全部替换一下)。

有可能,不过确实没什么提升空间了。

我不太用 early-init.el :expressionless_face:

好使,我的老破小虚机 win emacs-kl 启动时间从 3.7s 缩到 3.2s 了

1 个赞

:+1:

不过 3.7s 和 3.2s 分别是?具体来说,没用优化,用了 Emacs 31 那个,和我的包的时间分别是?

这个配置加到 init.el 启动速度从0.9秒提升到0.8秒。

(use-package persistent-cached-load-filter :ensure t
  :vc (:url "https://github.com/include-yy/persistent-cached-load-filter" :branch "main" :rev :newest)
  :config
  (persistent-cached-load-filter-easy-setup))

这是我的配置 GitHub - ltylty/.emacs.d

1 个赞

看来启动够快的话提升就比较小了,不过蚊子腿也是肉 :laughing:

1 个赞

在 powershell 用下边的指令测试的,长的自然是用之前的时间

Measure-Command { emacs --eval '(kill-emacs)' }
1 个赞

感觉对 windows 这种文件系统 io 比较昂贵的系统比较有用。在 macOS和 Linux 这种io很便宜的系统可能就帮助不大了哈哈

是这样的,zsbd

测试了下,放early-init.el需要在package-initialize之后,不然modus-theme会报错,很神秘。

early-init的代码在package-initialize之前,除非把整个包的代码塞到early-init 里面,或者在early-init里面做包初始化,不然可能会有问题

1 个赞

确实,在 wsl1 里也没啥效果

1 个赞