从系统层面分析Emacs的启动性能

现在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搜索情况,它们可能来自其它的函数调用。

7 个赞