歡迎光臨
每天分享高質量文章

降低 Emacs 啟動時間的高階技術 | Linux 中國

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

— Joe Schafer

 

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

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

從 .emacs.d 的失敗到現在

我最近宣佈了 .emacs.d 的第三次失敗,並完成了第四次 Emacs 配置的迭代。演化過程為:

1. 複製並貼上 elisp 片段到 ~/.emacs 中,希望它能工作。
2. 藉助 el-get 來以更結構化的方式來管理依賴關係。
3. 放棄自己從零配置,以 Spacemacs 為基礎。
4. 厭倦了 Spacemacs 的複雜性,基於 use-package 重寫配置。

本文匯聚了三次重寫和建立 《Emacs Start Up Profiler[1]》過程中的技巧。非常感謝 Spacemacs、use-package 等背後的團隊。沒有這些無私的志願者,這項任務將會困難得多。

不過守護行程樣式又如何呢

在我們開始之前,讓我反駁一下最佳化 Emacs 時的常見觀念:“Emacs 旨在作為守護行程來執行的,因此你只需要執行一次而已。”

這個觀點很好,只不過:

◈ 速度總是越快越好。
◈ 配置 Emacs 時,可能會有不得不透過重啟 Emacs 的情況。例如,你可能為 post-command-hook 添加了一個執行緩慢的 lambda 函式,很難刪掉它。
◈ 重啟 Emacs 能幫你驗證不同會話之間是否還能保留配置。

1、估算當前以及最佳的啟動時間

第一步是測量當前的啟動時間。最簡單的方法就是在啟動時顯示後續步驟進度的資訊。

  1. ;; Use a hook so the message doesn't get clobbered by other messages.
  2. (add-hook 'emacs-startup-hook
  3. (lambda ()
  4. (message "Emacs ready in %s with %d garbage collections."
  5. (format "%.2f seconds"
  6. (float-time
  7. (time-subtract after-init-time before-init-time)))
  8. gcs-done)))

第二步、測量最佳的啟動速度,以便瞭解可能的情況。我的是 0.3 秒。

  1. # -q ignores personal Emacs files but loads the site files.
  2. emacs -q --eval='(message "%s" (emacs-init-time))'
  3. ;; For macOS users:
  4. open -n /Applications/Emacs.app --args -q --eval='(message "%s" (emacs-init-time))'

2、檢測 Emacs 啟動指標對你大有幫助

Emacs StartUp Profiler[1]》(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 秒。

  1. ;; Make startup faster by reducing the frequency of garbage
  2. ;; collection. The default is 800 kilobytes. Measured in bytes.
  3. (setq gc-cons-threshold (* 50 1000 1000))
  4. ;; The rest of the init file.
  5. ;; Make gc pauses faster by decreasing the threshold.
  6. (setq gc-cons-threshold (* 2 1000 1000))

~/.emacs.d/init.el

4、不要 require 任何東西,而是使用 use-package 來自動載入

讓 Emacs 變壞的最好方法就是減少要做的事情。require 會立即載入源檔案,但是很少會出現需要在啟動階段就立即需要這些功能的。

在 use-package[4] 中你只需要宣告好需要哪個包中的哪個功能,use-package 就會幫你完成正確的事情。它看起來是這樣的:

  1. (use-package evil-lisp-state ; the Melpa package name
  2. :defer t ; autoload this package
  3. :init ; Code to run immediately.
  4. (setq evil-lisp-state-global nil)
  5. :config ; Code to run after the package is loaded.
  6. (abn/define-leader-keys "k" evil-lisp-state-map))

可以透過檢視 features 變數來檢視 Emacs 現在載入了那些包。想要更好看的輸出可以使用 lpkg explorer[5] 或者我在 abn-funcs-benchmark.el[6] 中的變體。輸出看起來類似這樣的:

  1. 479 features currently loaded
  2. - abn-funcs-benchmark: /Users/jschaf/.dotfiles/emacs/funcs/abn-funcs-benchmark.el
  3. - evil-surround: /Users/jschaf/.emacs.d/elpa/evil-surround-20170910.1952/evil-surround.elc
  4. - misearch: /Applications/Emacs.app/Contents/Resources/lisp/misearch.elc
  5. - multi-isearch: nil
  6. - <many more>

5、不要使用輔助函式來設定樣式

通常,Emacs 包會建議透過執行一個輔助函式來設定鍵系結。下麵是一些例子:

◈ (evil-escape-mode)
◈ (windmove-default-keybindings) ; 設定快捷鍵。
◈ (yas-global-mode 1) ; 複雜的片段配置。

可以透過 use-package 來對此進行重構以提高啟動速度。這些輔助函式只會讓你立即載入那些尚用不到的包。

下麵這個例子告訴你如何自動載入 evil-escape-mode

  1. ;; The definition of evil-escape-mode.
  2. (define-minor-mode evil-escape-mode
  3. (if evil-escape-mode
  4. (add-hook 'pre-command-hook 'evil-escape-pre-command-hook)
  5. (remove-hook 'pre-command-hook 'evil-escape-pre-command-hook)))
  6. ;; Before:
  7. (evil-escape-mode)
  8. ;; After:
  9. (use-package evil-escape
  10. :defer t
  11. ;; Only needed for functions without an autoload comment (;;;###autoload).
  12. :commands (evil-escape-pre-command-hook)
  13. ;; Adding to a hook won't load the function until we invoke it.
  14. ;; With pre-command-hook, that means the first command we run will
  15. ;; load evil-escape.
  16. :init (add-hook 'pre-command-hook 'evil-escape-pre-command-hook))

下麵來看一個關於 org-babel 的例子,這個例子更為複雜。我們通常的配置時這樣的:

  1. (org-babel-do-load-languages
  2. 'org-babel-load-languages
  3. '((shell . t)
  4. (emacs-lisp . nil)))

這不是個好的配置,因為 org-babel-do-load-languages 定義在 org.el 中,而該檔案有超過 2 萬 4 千行的程式碼,需要花 0.2 秒來載入。透過檢視原始碼可以看到 org-babel-do-load-languages 僅僅只是載入 ob- 包而已,像這樣:

  1. ;; From org.el in the org-babel-do-load-languages function.
  2. (require (intern (concat "ob-" lang)))

而在 ob-.el 檔案中,我們只關心其中的兩個方法 org-babel-execute: 和 org-babel-expand-body:。我們可以延時載入 org-babel 相關功能而無需呼叫 org-babel-do-load-languages,像這樣:

  1. ;; Avoid `org-babel-do-load-languages' since it does an eager require.
  2. (use-package ob-python
  3. :defer t
  4. :ensure org-plus-contrib
  5. :commands (org-babel-execute:python))
  6. (use-package ob-shell
  7. :defer t
  8. :ensure org-plus-contrib
  9. :commands
  10. (org-babel-execute:sh
  11. org-babel-expand-body:sh
  12. org-babel-execute:bash
  13. org-babel-expand-body:bash))

6、使用惰性定時器來推遲載入非立即需要的包

我推遲載入了 9 個包,這幫我節省了 0.4 秒

有些包特別有用,你希望可以很快就能使用它們,但是它們本身在 Emacs 啟動過程中又不是必須的。這些軟體包包括:

◈ recentf:儲存最近的編輯過的那些檔案。
◈ saveplace:儲存訪問過檔案的游標位置。
◈ server:開啟 Emacs 守護行程。
◈ autorevert:自動多載被修改過的檔案。
◈ paren:高亮匹配的括號。
◈ projectile:專案管理工具。
◈ whitespace:高亮行尾的空格。

不要 require 這些軟體包,而是等到空閑 N 秒後再載入它們。我在 1 秒後載入那些比較重要的包,在 2 秒後載入其他所有的包。

  1. (use-package recentf
  2. ;; Loads after 1 second of idle time.
  3. :defer 1)
  4. (use-package uniquify
  5. ;; Less important than recentf.
  6. :defer 2)

不值得的最佳化

不要費力把你的 Emacs 配置檔案編譯成位元組碼了。這隻節省了大約 0.05 秒。把配置檔案編譯成位元組碼還可能導致源檔案與編譯後的檔案不一致從而難以重現錯誤進行除錯。


via: https://blog.d46.us/advanced-emacs-startup/

作者:Joe Schafer[8] 選題:lujun9972 譯者:lujun9972 校對:wxy

贊(0)

分享創造快樂