降低 Emacs 啟動時間的高階技術

2019-03-17 10:30:00

Emacs Start Up Profiler》 的作者教你六項減少 Emacs 啟動時間的技術。

簡而言之:做下面幾個步驟:

  1. 使用 Esup 進行效能檢測。
  2. 調整垃圾回收的閥值。
  3. 使用 use-package 來自動(延遲)載入所有東西。
  4. 不要使用會引起立即載入的輔助函數。
  5. 參考我的 設定

從 .emacs.d 的失敗到現在

我最近宣布了 .emacs.d 的第三次失敗,並完成了第四次 Emacs 設定的疊代。演化過程為:

  1. 拷貝並貼上 elisp 片段到 ~/.emacs 中,希望它能工作。
  2. 借助 el-get 來以更結構化的方式來管理依賴關係。
  3. 放棄自己從零設定,以 Spacemacs 為基礎。
  4. 厭倦了 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)將會給你頂層語句執行的詳細指標。

esup.png

圖 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 秒。把組態檔編譯成位元組碼還可能導致原始檔與編譯後的檔案不一致從而難以重現錯誤進行偵錯。