Speeding Up Emacs and Parsing Emacs Lisp from Emacs Lisp
13 Apr 2013, 08:43 by Giorgos KeramidasI recently spent a bit of time to clean up all the cruft that my ~/.emacs
file and my ~/elisp
directory had accumulated. I have been using a multi-file setup to configure my Emacs sessions,
since at least 2008. This turned out to be a royal mess after 5+ years of patching stuff without a
very clear plan or structure. The total line-count of both my ~/.emacs
and all the *.el
files I
had imported into my ~/elisp
directory was almost 20,000 lines of code:
$ wc -l BACKUP/.emacs \$( find BACKUP/elisp -name '\*.el') 119 BACKUP/.emacs 84 BACKUP/elisp/keramida-w3m.el 90 BACKUP/elisp/keramida-keys.el 156 BACKUP/elisp/keramida-irc.el 5449 BACKUP/elisp/erlang.el 892 BACKUP/elisp/fill-column-indicator.el 344 BACKUP/elisp/keramida-erc.el 87 BACKUP/elisp/keramida-chrome.el 89 BACKUP/elisp/keramida-autoload.el 141 BACKUP/elisp/keramida-ui.el 42 BACKUP/elisp/keramida-slime.el 1082 BACKUP/elisp/ace-jump-mode.el 2 BACKUP/elisp/scala-mode2/scala-mode2-pkg.el 907 BACKUP/elisp/scala-mode2/scala-mode2-indent.el 26 BACKUP/elisp/scala-mode2/scala-mode2-lib.el 502 BACKUP/elisp/scala-mode2/scala-mode2-fontlock.el 37 BACKUP/elisp/scala-mode2/scala-mode2-map.el 808 BACKUP/elisp/scala-mode2/scala-mode2-syntax.el 111 BACKUP/elisp/scala-mode2/scala-mode2.el 121 BACKUP/elisp/scala-mode2/scala-mode2-paragraph.el 1103 BACKUP/elisp/php-mode.el 142 BACKUP/elisp/themes/cobalt-theme.el 665 BACKUP/elisp/themes/zenburn-theme.el 142 BACKUP/elisp/themes/sublime-themes/cobalt-theme.el 80 BACKUP/elisp/themes/tomorrow-night-blue-theme.el 80 BACKUP/elisp/themes/tomorrow-night-eighties-theme.el 115 BACKUP/elisp/themes/tomorrow-theme.el 80 BACKUP/elisp/themes/tomorrow-night-bright-theme.el 339 BACKUP/elisp/cmake-mode.el 95 BACKUP/elisp/keramida-cc-extra.el 1341 BACKUP/elisp/lua-mode.el 2324 BACKUP/elisp/markdown-mode.el 184 BACKUP/elisp/rcirc-notify.el 167 BACKUP/elisp/keramida-defaults.el 203 BACKUP/elisp/keramida-hooks.el 43 BACKUP/elisp/keramida-lang.el 435 BACKUP/elisp/edit-server.el 709 BACKUP/elisp/slang-mode.el 66 BACKUP/elisp/keramida-eshell.el 19402 total
20,000 lines of code is far too much bloat. It’s obvious that this was getting out of hand, especially if you consider that I had full configuration files for at least two different IRC clients (rcirc and erc) in this ever growing blob of complexity.
What I did was make a backup copy of everything in ~/BACKUP
and start over. This time I decided to
go a different route from 2008 though. All my configuration lives in a single file, in ~/.emacs
,
and I threw away any library from my old ~/elisp
tree which I haven’t actively used in the past
few weeks. I imported the rest of them into the standard user-emacs-directory
of modern Emacsen:
at ~/.emacs.d/
. I also started using eval-after-load
pretty extensively, to speed up the startup
of Emacs, and only configure extras after the related packages are loaded. This means I could trim
down the list of preloaded packages even more.
The result, as I tweeted yesterday was an impressive speedup of the entire startup process of Emacs. Now it can start, load everything and print a message in approximately 0.028 seconds, which is more than 53 times faster than the \~1.5 seconds it required before the cleanup!
I suspected that the main contributor to this speedup was the increased use of eval-after-load
forms, but what percentage of the entire file used them?
So I wrote a tiny bit of Emacs Lisp to count how many times each top-level forms appears in my new
~/.emacs
file:
(defun file-forms-list (file-name) (let ((file-forms nil)) ;; Keep reading Lisp expressions, until we hit EOF, and just add one ;; entry for each toplevel form to `file-forms'. (condition-case err (with-temp-buffer (insert-file file-name) (goto-char (point-min)) (while (< (point) (point-max)) (let* ((expr (read (current-buffer))) (form (first expr))) (setq file-forms (cons form file-forms))))) (end-of-file nil)) (reverse file-forms))) (defun file-forms-alist (file-name) (let ((forms-table (make-hash-table :test #'equal))) ;; Build a hash that maps form-name => count for all the ;; top-level forms of the `file-name' file. (dolist (form (file-forms-list file-name)) (let ((form-name (format "%s" form))) (puthash form-name (1+ (gethash form-name forms-table 0)) forms-table))) ;; Convert the hash table to an alist of the form: ;; ((form-name . count) (form-name-2 . count-2) ...) (let ((forms-alist nil)) (maphash (lambda (form-name form-count) (setq forms-alist (cons (cons form-name form-count) forms-alist))) forms-table) forms-alist))) (progn (insert "\n") (insert (format "%7s %s\n" "COUNT" "FORM-NAME")) (let ((total-forms 0)) (dolist (fc (sort (file-forms-alist "~/.emacs") (lambda (left right) (> (cdr left) (cdr right))))) (insert (format "%7d %s\n" (cdr fc) (car fc))) (setq total-forms (+ total-forms (cdr fc)))) (insert (format "%7d %s\n" total-forms "TOTAL"))))
Evaluating this in a scratch buffer shows output like this:
COUNT FORM-NAME 32 setq-default 24 eval-after-load 14 set-face-attribute 14 global-set-key 5 autoload 4 require 4 setq 4 put 3 defun 2 when 1 add-hook 1 let 1 set-display-table-slot 1 fset 1 tool-bar-mode 1 scroll-bar-mode 1 menu-bar-mode 1 ido-mode 1 global-hl-line-mode 1 show-paren-mode 1 iswitchb-mode 1 global-font-lock-mode 1 cua-mode 1 column-number-mode 1 add-to-list 1 prefer-coding-system 122 TOTAL
This showed that I’m still using a lot of setq-default
forms: 26.23% of the top-level forms are of
this type. Some of these may still be candidates for lazy initialization, since I can see that many
of them are indeed mode-specific, like these two:
(setq-default diff-switches "-u") (setq-default ps-font-size '(8 . 10))
But eval-after-load
is a close second, with 19.67% of all the top-level forms. That seems to agree
with the original idea of speeding up the startup of everything by delaying package-loading and
configuration until it’s actually needed.
10 of the remaining forms are one-off mode setting calls, like (tool-bar-mode -1)
, so 8.2% of the
total calls is probably going to stay this way for a long time. That’s probably ok though, since the
list includes several features I find really useful, very very often.