降低 Emacs 啟動時間的高級技術
《Emacs Start Up Profiler》 的作者教你六項減少 Emacs 啟動時間的技術。
簡而言之:做下面幾個步驟:
- 使用 Esup 進行性能檢測。
- 調整垃圾回收的閥值。
- 使用 use-package 來自動(延遲)載入所有東西。
- 不要使用會引起立即載入的輔助函數。
- 參考我的 配置。
從 .emacs.d 的失敗到現在
我最近宣布了 .emacs.d 的第三次失敗,並完成了第四次 Emacs 配置的迭代。演化過程為:
- 拷貝並粘貼 elisp 片段到
~/.emacs
中,希望它能工作。 - 藉助
el-get
來以更結構化的方式來管理依賴關係。 - 放棄自己從零配置,以 Spacemacs 為基礎。
- 厭倦了 Spacemacs 的複雜性,基於
use-package
重寫配置。
本文匯聚了三次重寫和創建 《Emacs Start Up Profiler》過程中的技巧。非常感謝 Spacemacs、use-package 等背後的團隊。沒有這些無私的志願者,這項任務將會困難得多。
不過守護進程模式又如何呢
在我們開始之前,讓我反駁一下優化 Emacs 時的常見觀念:「Emacs 旨在作為守護進程來運行的,因此你只需要運行一次而已。」
這個觀點很好,只不過:
- 速度總是越快越好。
- 配置 Emacs 時,可能會有不得不通過重啟 Emacs 的情況。例如,你可能為
post-command-hook
添加了一個運行緩慢的lambda
函數,很難刪掉它。 - 重啟 Emacs 能幫你驗證不同會話之間是否還能保留配置。
1、估算當前以及最佳的啟動時間
第一步是測量當前的啟動時間。最簡單的方法就是在啟動時顯示後續步驟進度的信息。
;; Use a hook so the message doesn't get clobbered by other messages.
(add-hook 'emacs-startup-hook
(lambda ()
(message "Emacs ready in %s with %d garbage collections."
(format "%.2f seconds"
(float-time
(time-subtract after-init-time before-init-time)))
gcs-done)))
第二步、測量最佳的啟動速度,以便了解可能的情況。我的是 0.3 秒。
# -q ignores personal Emacs files but loads the site files.
emacs -q --eval='(message "%s" (emacs-init-time))'
;; For macOS users:
open -n /Applications/Emacs.app --args -q --eval='(message "%s" (emacs-init-time))'
2、檢測 Emacs 啟動指標對你大有幫助
《Emacs StartUp Profiler》(ESUP)將會給你頂層語句執行的詳細指標。
圖 1: Emacs Start Up Profiler 截圖
警告:Spacemacs 用戶需要注意,ESUP 目前與 Spacemacs 的 init.el 文件有衝突。遵照 https://github.com/jschaf/esup/issues/48 上說的進行升級。
3、調高啟動時垃圾回收的閥值
這為我節省了 0.3 秒。
Emacs 默認值是 760kB,這在現代機器看來極其保守。真正的訣竅在於初始化完成後再把它降到合理的水平。這為我節省了 0.3 秒。
;; Make startup faster by reducing the frequency of garbage
;; collection. The default is 800 kilobytes. Measured in bytes.
(setq gc-cons-threshold (* 50 1000 1000))
;; The rest of the init file.
;; Make gc pauses faster by decreasing the threshold.
(setq gc-cons-threshold (* 2 1000 1000))
~/.emacs.d/init.el
4、不要 require 任何東西,而是使用 use-package 來自動載入
讓 Emacs 變壞的最好方法就是減少要做的事情。require
會立即載入源文件,但是很少會出現需要在啟動階段就立即需要這些功能的。
在 use-package 中你只需要聲明好需要哪個包中的哪個功能,use-package
就會幫你完成正確的事情。它看起來是這樣的:
(use-package evil-lisp-state ; the Melpa package name
:defer t ; autoload this package
:init ; Code to run immediately.
(setq evil-lisp-state-global nil)
:config ; Code to run after the package is loaded.
(abn/define-leader-keys "k" evil-lisp-state-map))
可以通過查看 features
變數來查看 Emacs 現在載入了那些包。想要更好看的輸出可以使用 lpkg explorer 或者我在 abn-funcs-benchmark.el 中的變體。輸出看起來類似這樣的:
479 features currently loaded
- abn-funcs-benchmark: /Users/jschaf/.dotfiles/emacs/funcs/abn-funcs-benchmark.el
- evil-surround: /Users/jschaf/.emacs.d/elpa/evil-surround-20170910.1952/evil-surround.elc
- misearch: /Applications/Emacs.app/Contents/Resources/lisp/misearch.elc
- multi-isearch: nil
- <many more>
5、不要使用輔助函數來設置模式
通常,Emacs 包會建議通過運行一個輔助函數來設置鍵綁定。下面是一些例子:
(evil-escape-mode)
(windmove-default-keybindings) ; 設置快捷鍵。
(yas-global-mode 1) ; 複雜的片段配置。
可以通過 use-package
來對此進行重構以提高啟動速度。這些輔助函數只會讓你立即載入那些尚用不到的包。
下面這個例子告訴你如何自動載入 evil-escape-mode
。
;; The definition of evil-escape-mode.
(define-minor-mode evil-escape-mode
(if evil-escape-mode
(add-hook 'pre-command-hook 'evil-escape-pre-command-hook)
(remove-hook 'pre-command-hook 'evil-escape-pre-command-hook)))
;; Before:
(evil-escape-mode)
;; After:
(use-package evil-escape
:defer t
;; Only needed for functions without an autoload comment (;;;###autoload).
:commands (evil-escape-pre-command-hook)
;; Adding to a hook won't load the function until we invoke it.
;; With pre-command-hook, that means the first command we run will
;; load evil-escape.
:init (add-hook 'pre-command-hook 'evil-escape-pre-command-hook))
下面來看一個關於 org-babel
的例子,這個例子更為複雜。我們通常的配置時這樣的:
(org-babel-do-load-languages
'org-babel-load-languages
'((shell . t)
(emacs-lisp . nil)))
這不是個好的配置,因為 org-babel-do-load-languages
定義在 org.el
中,而該文件有超過 2 萬 4 千行的代碼,需要花 0.2 秒來載入。通過查看源代碼可以看到 org-babel-do-load-languages
僅僅只是載入 ob-<lang>
包而已,像這樣:
;; From org.el in the org-babel-do-load-languages function.
(require (intern (concat "ob-" lang)))
而在 ob-<lang>.el
文件中,我們只關心其中的兩個方法 org-babel-execute:<lang>
和 org-babel-expand-body:<lang>
。我們可以延時載入 org-babel 相關功能而無需調用 org-babel-do-load-languages
,像這樣:
;; Avoid `org-babel-do-load-languages' since it does an eager require.
(use-package ob-python
:defer t
:ensure org-plus-contrib
:commands (org-babel-execute:python))
(use-package ob-shell
:defer t
:ensure org-plus-contrib
:commands
(org-babel-execute:sh
org-babel-expand-body:sh
org-babel-execute:bash
org-babel-expand-body:bash))
6、使用惰性定時器來推遲載入非立即需要的包
我推遲載入了 9 個包,這幫我節省了 0.4 秒。
有些包特別有用,你希望可以很快就能使用它們,但是它們本身在 Emacs 啟動過程中又不是必須的。這些軟體包包括:
recentf
:保存最近的編輯過的那些文件。saveplace
:保存訪問過文件的游標位置。server
:開啟 Emacs 守護進程。autorevert
:自動重載被修改過的文件。paren
:高亮匹配的括弧。projectile
:項目管理工具。whitespace
:高亮行尾的空格。
不要 require
這些軟體包,而是等到空閑 N 秒後再載入它們。我在 1 秒後載入那些比較重要的包,在 2 秒後載入其他所有的包。
(use-package recentf
;; Loads after 1 second of idle time.
:defer 1)
(use-package uniquify
;; Less important than recentf.
:defer 2)
不值得的優化
不要費力把你的 Emacs 配置文件編譯成位元組碼了。這隻節省了大約 0.05 秒。把配置文件編譯成位元組碼還可能導致源文件與編譯後的文件不一致從而難以重現錯誤進行調試。
via: https://blog.d46.us/advanced-emacs-startup/
作者:Joe Schafer 選題:lujun9972 譯者:lujun9972 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive