Don't Stop the World


Emacs being essentially an oversized, graphical REPL[1], is one of the reasons why it comes with Garbage Collection (which I will abbreviate from now on) as feature. This essentially means that you’re able to inspect its state, customize its tunables to change Emacs performance and can even force an explicit GC to happen. The specific type of GC Emacs employs is a simple Mark and Sweep method; due to its weakness of introducing periodical freezes, it is also known as “Stop the World” GC. The main tunable responsible for the frequency and length of pauses would be the gc-cons-threshold variable which is set by default to a measly 800 thousand bytes.

A lesser known feature of Emacs would be dynamic scoping. To put it short, it allows a variable to be accessible from its subroutines and therefore allows one to override the value of an existing variable for the lifetime of a subroutine. It is possible to enable lexical scoping for saner scoping rules on a per-file basis, by doing so the language behaves much more like Common Lisp where top-level definitions are dynamically bound and everything else is lexically bound.

So, what could GC and dynamic scoping have to do with the horrors of Emacs? Well, there is Emacs Lisp code that binds the gc-cons-threshold variable to a much higher value than usual.

(defvar calc-aborted-prefix nil)
(defvar calc-start-time nil)
(defvar calc-command-flags nil)
(defvar calc-final-point-line)
(defvar calc-final-point-column)
;;; Note that modifications to this function may break calc-pass-errors.
(defun calc-do (do-body &optional do-slow)
  (let* ((calc-command-flags nil)
         (calc-start-time (and calc-timing (not calc-start-time)
                               (require 'calc-ext)
         (gc-cons-threshold (max gc-cons-threshold
                                 (if calc-timing 2000000 100000)))
         calc-final-point-line calc-final-point-column)
    (setq calc-aborted-prefix "")
        (condition-case err
              (if calc-embedded-info
              (and (eq calc-algebraic-mode 'total)
                   (require 'calc-ext)
                   (use-local-map calc-alg-map))
              (when (and do-slow calc-display-working-message)
                (message "Working...")
                (calc-set-command-flag 'clear-message))
              (funcall do-body)
              (setq calc-aborted-prefix nil)
              (when (memq 'renum-stack calc-command-flags)
              (when (memq 'clear-message calc-command-flags)
                (message "")))
           (if (and (eq (car err) 'error)
                    (stringp (nth 1 err))
                    (string-match "max-specpdl-size\\|max-lisp-eval-depth"
                                  (nth 1 err)))
               (error "Computation got stuck or ran too long.  Type `M' to increase the limit")
             (setq calc-aborted-prefix nil)
             (signal (car err) (cdr err)))))
      (when calc-aborted-prefix
        (calc-record "<Aborted>" calc-aborted-prefix))
      (and calc-start-time
           (let* ((calc-internal-prec 12)
                  (calc-date-format nil)
                  (end-time (current-time-string))
                  (time (if (equal calc-start-time end-time)
                           (calcFunc-unixtime (math-parse-date end-time) 0)
                           (calcFunc-unixtime (math-parse-date calc-start-time)
             (if (math-lessp 1 time)
                 (calc-record time "(t)"))))
      (or (memq 'no-align calc-command-flags)
          (derived-mode-p 'calc-trail-mode)
      (and (memq 'position-point calc-command-flags)
           (if (derived-mode-p 'calc-mode)
                 (goto-char (point-min))
                 (forward-line (1- calc-final-point-line))
                 (move-to-column calc-final-point-column))
               (goto-char (point-min))
               (forward-line (1- calc-final-point-line))
               (move-to-column calc-final-point-column))))
      (unless (memq 'keep-flags calc-command-flags)
          (setq calc-inverse-flag nil
                calc-hyperbolic-flag nil
                calc-option-flag nil
                calc-keep-args-flag nil)))
      (when (memq 'do-edit calc-command-flags)
        (switch-to-buffer (get-buffer-create "*Calc Edit*")))
      (when calc-embedded-info
  (identity nil))  ; allow a GC after timing is done

This one is from calc, a package allowing one to do advanced calculation in Emacs. Apparently the function is an entry point and not only raises the GC limit, but also tries dealing with other critical errors by doing a regular expression match on error messages and measuring run times, then suggesting an increase of these other limits.

If you’re asking yourself why one would possibly want to increase the GC limit temporarily, another place of Emacs using that trick should make it clearer.

;; Read Lisp objects.  Temporarily increase `gc-cons-threshold' to
;; prevent a GC that would not free any memory.

So that’s why. Pauses are only perceived as OK if they free memory. Reminds me a bit of the story about Erik Naggum deactivating the garbage collection messages Emacs displayed back then to see whether it would end the complaints of it being slow coming from a group of Emacs users. Surprisingly enough, it did.

I do wonder whether this incident made the Emacs core developers change that default. If you wish to feel like you’re in the nineties, just set garbage-collection-messages to t.

[1]Emacs also comes with its own textual REPL, IELM. It is tremendously useful for writing Emacs Lisp code in a more traditional style.