现在emacs-31分支已经进入了freezing阶段,除了重大bug修复,新的改动不会再添加到emacs-31分支中。于是,最近我把我的emacs构建更新到了emacs-31分支上。
我实现了一个简单的load-path-filter-function,发现整体启动速度提升了约0.1s(提升并没有期待的那么高,但比较符合strace测出来的数据),openat的命中率提高到了1/3(主要是suffixes不支持过滤造成的),调用次数也下降了20多倍(这次我起的是GUI,会加载比TUI启动更多的包)。
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- -------------------
23.84 0.020608 0 23350 rt_sigprocmask
12.80 0.011070 0 11248 timerfd_settime
12.47 0.010783 0 11248 timer_settime
11.19 0.009674 5 1865 mmap
9.59 0.008289 0 22616 21688 faccessat2
8.08 0.006987 99 70 munmap
6.61 0.005712 0 6211 4696 openat
3.44 0.002973 0 3326 1 read
2.60 0.002246 449 5 wait4
1.66 0.001431 0 1564 close
1.56 0.001353 0 1598 fstat
0.98 0.000846 5 166 ppoll
0.53 0.000457 0 904 lseek
0.49 0.000421 2 200 write
0.47 0.000410 0 504 rt_sigaction
0.47 0.000403 0 756 73 futex
0.37 0.000320 35 9 fallocate
0.36 0.000314 0 391 mprotect
0.35 0.000303 0 1013 1010 readlink
0.33 0.000287 14 20 pselect6
0.29 0.000247 0 405 brk
0.19 0.000162 2 59 sendmsg
0.18 0.000155 8 19 memfd_create
0.16 0.000141 0 328 fcntl
0.15 0.000128 1 78 alarm
0.15 0.000128 3 36 getdents64
0.15 0.000127 2 45 2 rt_sigreturn
0.14 0.000117 1 74 37 recvmsg
0.07 0.000062 3 18 ftruncate
0.06 0.000049 0 81 10 newfstatat
0.05 0.000041 3 11 pread64
0.04 0.000034 0 226 185 access
0.03 0.000027 9 3 symlink
0.03 0.000027 3 7 tgkill
0.02 0.000020 0 21 getpid
0.02 0.000019 4 4 unlink
0.02 0.000017 1 9 clone3
0.01 0.000012 1 10 gettid
0.01 0.000009 0 13 uname
0.01 0.000005 2 2 fchmodat
0.00 0.000004 0 15 poll
0.00 0.000004 0 5 1 readlinkat
0.00 0.000003 0 8 sendto
0.00 0.000003 3 1 arch_prctl
0.00 0.000003 1 3 getrandom
0.00 0.000003 3 1 rseq
0.00 0.000002 0 3 socket
0.00 0.000002 0 3 connect
0.00 0.000002 2 1 statfs
0.00 0.000002 2 1 set_tid_address
0.00 0.000002 2 1 set_robust_list
0.00 0.000002 0 34 prlimit64
0.00 0.000001 1 1 ioctl
0.00 0.000001 1 1 pwrite64
0.00 0.000001 0 4 madvise
0.00 0.000001 0 2 2 mkdir
0.00 0.000001 0 3 geteuid
0.00 0.000001 1 1 sched_getaffinity
0.00 0.000001 1 1 inotify_add_watch
0.00 0.000001 0 4 eventfd2
0.00 0.000001 1 1 inotify_init1
0.00 0.000001 0 9 statx
0.00 0.000000 0 3 dup2
0.00 0.000000 0 2 getsockname
0.00 0.000000 0 1 execve
0.00 0.000000 0 2 umask
0.00 0.000000 0 3 getuid
0.00 0.000000 0 1 getgid
0.00 0.000000 0 1 getegid
0.00 0.000000 0 1 getpgrp
0.00 0.000000 0 2 getresuid
0.00 0.000000 0 2 getresgid
0.00 0.000000 0 1 sigaltstack
0.00 0.000000 0 1 sched_getscheduler
0.00 0.000000 0 1 timer_create
0.00 0.000000 0 1 timerfd_create
0.00 0.000000 0 5 pipe2
0.00 0.000000 0 1 pidfd_open
------ ----------- ----------- --------- --------- -------------------
100.00 0.086456 0 88644 27705 total
让我觉得不舒服的地方是load-path-filter这个功能无法根据load的结果进行动态训练更新cache。目前我的策略是在load-path-filter-function里面提前查找文件的方式训练cache,validation会产生overhead。动态训练和静态生成cache相比不需要反复生成cache,你用的越多它就越聪明。如果你的cache文件因为意外(比如写回时发生故障)损坏了,手动把它删掉就可以了,下次启动时,只要保持my/lpf--cache-training开启,它就会开始从头重练。
以下是我的简易lpf实现,该实现没有使用任何库函数,因此你可以把它放在early-init.el中。startup.el中实现的load-path-filter-cache-directory-files要等到它加载之后才开始起作用,这会错过前面的很多优化点,毕竟load从一开始就在被调用了。
;; setup load-path filter
(defvar my/lpf--cache-file
(file-name-concat user-emacs-directory "load-path-filter-cache.el"))
(defvar my/lpf--cache
(if (file-readable-p my/lpf--cache-file)
(with-temp-buffer
(insert-file-contents my/lpf--cache-file)
(goto-char 0)
(condition-case nil
(read (current-buffer))
(error (make-hash-table :test #'equal))))
(make-hash-table :test #'equal))
"A cache used by function `my:lpf', which is a hash table whose each entry
is a FILE name, and value is filtered PATH.")
;; save cache when exit emacs
(add-hook 'kill-emacs-hook
#'(lambda ()
(with-temp-file my/lpf--cache-file
(insert (prin1-to-string my/lpf--cache)))))
(defvar my/lpf--cache-training t
"If this is non-nil, start training when my/lpf--cache missed.")
(defvar my/lpf--cache-validate t
"If this is non-nil, validate each entry found in cache before load.
If invalid, erase it from the cache.")
(defun my:lpf-match (path file suffixes)
(catch 'found
(dolist (x (mapcar #'(lambda (suf) (concat file suf)) suffixes))
(if-let* ((res (mapcan #'(lambda (p)
(when (file-readable-p (file-name-concat p x))
(list p)))
path)))
(throw 'found res)))))
(defun my:lpf-train (path file suffixes)
(if (or (file-directory-p file)
(not my/lpf--cache-training))
path
(when-let*
((val (my:lpf-match path file suffixes)))
(puthash file val my/lpf--cache))))
(defun my:lpf (path file suffixes)
(if-let* ((val (gethash file my/lpf--cache)))
;; cache hitted
(progn
(when (and my/lpf--cache-validate
(null (my:lpf-match val file suffixes)))
(setq val (my:lpf-train path file suffixes)))
;; append val to the beginning of path, this ensures it works correctly
;; when there are dirty values that are not validated
(append val path))
;; otherwise, start training, this may be painful when hit rate is low.
(or (my:lpf-train path file suffixes)
path)))
(setq load-path-filter-function #'my:lpf)
此外,我观察到strace日志中,仍然有一些无法过滤掉的load-path搜索情况,它们可能来自其它的函数调用。