Speeding Up Emacs and Parsing Emacs Lisp from Emacs Lisp

I 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.

About these ads
This entry was posted in Computers, Emacs, Free software, GNU/Linux, Linux, Lisp, Open source, Programming, Software and tagged , , , , , , , , . Bookmark the permalink.

9 Responses to Speeding Up Emacs and Parsing Emacs Lisp from Emacs Lisp

  1. alexkaltsas says:

    Now you won’t be able to blink during start up… :D :D :D

  2. bjr says:

    Good work, there is a missing “)” in the 8th line of function file-forms-list. Would be happy to have similar startup times !!

  3. John Wiegley says:

    Note that my `use-package` macro can greatly simplify — and automate — the proper use of eval-after-load. See https://github.com/jwiegley/use-package.

    • keramida says:

      John, that’s a wonderful package! Thanks :-)

      I’ve been wondering if there’s a good way I can organize the current configuration “by package” instead of the current “by feature type” scheme. I think I’ll give `use-package’ a try.

      • John Wiegley says:

        If you clone my dot-emacs repository from GitHub, you can see lots of examples of how I use use-package. Even though I have over 80 packages in my configuration, load-time is still around a second — and that’s without byte-compiling my init file!

  4. Chad says:

    Depending on your platform, you might be able to move things like (tool-bar-mode -1) into system resources (defaults, X resources, etc). I don’t generally advocate taking configuration settings out of elisp, but disabling some of the Emacs chrome at the window-system level can prevent emacs from building them before it loads .emacs, speeding things up over-all.

  5. Reblogged this on Duong Bao Duy and commented:
    Good work, thanks to author.

Comments are closed.