The Mastering Emacs ebook cover art

Emacs 27 Edition is out now!

$39.99 Buy Now

Learn More

Speed up Emacs with libjansson and native elisp compilation

Cyril Northcote Parkinson wrote in his book Parkinson’s Law that “work expands so as to fill the time available for its completion.” If you squint your eyes, you’ll see that this is also the case for modern software, and in particular Emacs.

For every incrementation in performance, there will be a new gizmo to dial it back down again. Common sources of performance bottlenecks include completion frameworks; the Emacs client libraries that talk to Language Servers; and other highly trafficked code paths used widely in Emacs’s many packages.

There are some ministrations that balm the performance pains, though: Emacs 27 now supports libjansson, a C library for working with JSON. If you’re a heavy user of lsp-mode or eglot then I highly recommend you upgrade for that reason alone.

However, one change that did not make it into Emacs 27 – and the topic of today’s blog post – is an exciting experimental feature called “native compilation.” Briefly, Andrea Corallo discovered that you can use GCC to compile Emacs Lisp and reap the benefits of the speedups and, with a generous amount of elbow grease from Andrea over the past year, very few downsides — except the initial compilation speed, but more on that in a moment.

I believe this change is a significant step forward, even with the added dependencies in the build chain on libgccjit. I’ve been using the origin/native-comp branch in the Emacs git tree for a while now and I’ve not experienced any issues. Nevertheless this is an unreleased feature, and subject to changes and breakages.

Andrea’s notes, linked above, goes into all the gory details complete with diagrams and charts about the improved performance. However, synthetic benchmarks aside, there _is_ a difference in day-to-day Emacsing: Emacs is noticeably snappier and if you often struggle with performance issues I recommend you check it out. It’s very likely this feature will make it into Emacs 28, but that is years away.

As the feature’s not mainlined yet, you’ll have to compile it yourself. I’ve taken the liberty of writing a Dockerfile that demonstrates how to do it with Ubuntu 20.04. It’s honestly quite straightforward. But if you want to run native-comp Emacs on your soft-serve ice cream maker running DragonflyBSD because the heat is melting your ice cream… you’re on your own. Sorry.

To start with, you’ll need to clone Emacs from source:

$ git clone https://git.savannah.gnu.org/git/emacs.git
$ git checkout -b native-comp origin/feature/native-comp

Next, you can either build the Dockerfile, or simply run the commands manually on your system. There’s no need for Docker at all here; it’s a just useful vessel for capturing the requirements. Note: I’ve made no effort to compile Emacs with all the kitchen sink features.

Here’s the Dockerfile. I’ve deliberately not squashed everything into the minimal number of layers to make it more readable:

FROM ubuntu:20.04

# We assume the git repo's cloned outside and copied in, instead of
# cloning it in here. But that works, too.
WORKDIR /opt
COPY . /opt

LABEL MAINTAINER "Mickey Petersen at mastering emacs"

# Needed for add-apt-repository, et al.
#
# If you're installing this outside Docker you may not need this.
RUN apt-get update \
        && apt-get install -y \
        apt-transport-https \
        ca-certificates \
        curl \
        gnupg-agent \
        software-properties-common

# Needed for gcc-10 and the build process.
RUN add-apt-repository ppa:ubuntu-toolchain-r/ppa \
        && apt-get update -y \
        && apt-get install -y gcc-10 libgccjit0 libgccjit-10-dev

# Needed for fast JSON and the configure step
RUN apt-get install -y libjansson4 libjansson-dev git

# Shut up debconf as it'll fuss over postfix for no good reason
# otherwise. If you're doing this outside Docker, you do not need to
# do this.
ENV DEBIAN_FRONTEND=noninteractive

# Cheats' way of ensuring we get all the build deps for Emacs without
# specifying them ourselves. Enable source packages then tell apt to
# get all the deps for whatever Emacs version Ubuntu supports by
# default.
RUN sed -i 's/# deb-src/deb-src/' /etc/apt/sources.list \
    && apt-get update \
    && apt-get build-dep -y emacs

# Needed for compiling libgccjit or we'll get cryptic error messages
# about failing smoke tests.
ENV CC="gcc-10"

# Configure and run
RUN ./autogen.sh \
        && ./configure --with-nativecomp --with-mailutils

ENV JOBS=2
RUN make -j ${JOBS} && make install

ENTRYPOINT ["emacs"]

The whole process may take a while. The native compilation step’s not that quick, but infinitely better than it was just six months ago. On my workstation it took maybe 30 minutes in total. Grab a cup of coffee and a danish.

To test that both the fast JSON and native compilation is working you can evaluate the following elisp in Emacs:

(if (and (fboundp 'native-comp-available-p)
       (native-comp-available-p))
  (message "Native compilation is available")
(message "Native complation is *not* available"))

And for the JSON:

(if (functionp 'json-serialize)
  (message "Native JSON is available")
(message "Native JSON is *not* available"))

If it all looks good, you can even tell the native-comp system to automatically generate the natively compiled files when Emacs loads a new .elc file. Keep in mind this will freeze Emacs for a little while until it’s done. But once it’s done, it’s done:

(setq comp-deferred-compilation t)

I recommend you set it early in your init file so all future packages benefit from async compilation.

And.. that’s that. Enjoy your souped-up Emacs.