César Olea

Software Developer | CTO of LoanPro

11 Mar 2021

Native compilation and "pure" GTK in Emacs

There's some very exciting developments in the Emacs world: native compilation of elisp and a "pure" GTK frontend for Emacs.

"Pure" GTK frontend

Emacs is not a proper GTK application. There are many reasons for things being this way, but this fact prevents Emacs from benefiting from recent developments in GTK, for example hardware rendering, smooth scrolling, crisp font rendering, wayland support, among other things.

Native compilation

While the pgtk frontend of Emacs can make Emacs frames feel faster and more responsive, native compilation simply turbocharges elisp code execution. Native compilation, as the name implies, compiles all the elisp into native binaries that Emacs can dynamically load and execute. In benchmarks this is from 2.3x up to 42x faster(!!) with respect to the equivalent byte-code execution.

In practice, the end result is a faster Emacs experience overall. Everything is snappier, small lags with things like bringing up a buffer list, switching buffers, searching for candidates, autocompletion, using the agenda, all gone.

How to get pgtk and native compilation

Both pgtk and native compilation are not scheduled to land in Emacs 28, however there are people maintaining repositories in sync with Emacs 28 plus native compilation and pgtk.

The one I'm using I took from a AUR package in the user contrib repositores from Arch linux. The repo itself is here: https://github.com/flatwhatson/emacs

Compiling in Ubuntu 20.04

I'm currently using Ubuntu 20.04 at work so this is what we'll use here.

Install dependencies

  sudo add-apt-repository ppa:ubuntu-toolchain-r/ppa
  sudo apt update
  sudo apt install libxpm-dev libgif-dev libjpeg-dev libpng-dev libtiff-dev libx11-dev libncurses5-dev automake autoconf texinfo libgtk2.0-dev gcc-10 g++-10 libgccjit0 libgccjit-10-dev libjansson4 libjansson-dev

The "trick" here is to add ppa:ubuntu-toolchain-r as the version of libgccjit shipped with Ubuntu 20.04 won't work for with Emacs native compilation.

Clone and configure

The next step is to clone the repository, switch to the proper branch and configure everything for the final compilation step:

  git clone https://github.com/flatwhatson/emacs.git emacs-arch
  git checkout pgtk-nativecomp
  cd emacs-arch
  export CC=/usr/bin/gcc-10 CXX=/usr/bin/gcc-10
  ./autogen.sh
  ./configure --with-dbus --with-gif --with-jpeg --with-png --with-rsvg --with-tiff --with-xft --with-xpm --with-gpm=no --with-xwidgets --with-modules --with-native-compilation --with-pgtk --with-json CFLAGS="-O3 -mtune=native -march=native -fomit-frame-pointer"

If everything goes well, the next step is to compile Emacs.

Compilation

The final step is to compile Emacs. It will take very long as it needs to compile Emacs plus all the elisp code to native code. This step will only compile the elisp code that is part of Emacs, and not your own elisp code that is part of your configuration (we'll do that later).

  make -j2 NATIVE_FULL_AOT=1

The -j2 part is to tell the compiler how many cores to use for the compilation. You can adjust here depending on your own hardware. Nevertheless it will take a long time.

If everything goes well, you can execute your newly compiled Emacs by: ./src/emacs. At this point you have an Emacs distribution that is able to compile elisp to native code and load it dynamically. On top of that, the frontend used is a proper GTK application.

Compiling all custom elisp

As stated before, the compilation step will only compile elisp that is part of the Emacs distribution. We still have two more sources of elisp that we would like to compile: elisp from our own configuration or from a package repository (such as melpa), and elisp that we might have installed from our OS package manager.

Compiling our own configuration

We can add this to our configuration file to tell Emacs to compile elisp when it loads it for the first time:

  (when (fboundp 'native-compile-async)
    (setq comp-deferred-compilation t
          comp-deferred-compilation-black-list '("/mu4e.*\\.el$")))

With this configuration in place, whenever Emacs loads some elisp that is not compiled yet, it will compile it and load it. We are also blacklisting some modules because those were installed via the OS and we will compile them separately.

While this works, we can also tell Emacs to compile everything under certain directory. This can be useful to compile all our packages from melpa:

  (native-compile-async "~/.config/emacs/elpa/" 6 t)

Replace for the proper path of your melpa repository and the number of cores you want to use for the compilation process (6 in this case). Evaluate the form, and let Emacs do its magic.

Compiling elisp from OS packages

For system-wide packages that might need root permissions, navigate to the directory where the elisp source files are located and:

  sudo emacs -Q -batch -L . -f batch-native-compile *.el

Replace emacs with the full path of the Emacs binary we just compiled. In my case it is ~/workspace/emacs/src/emacs.

Verify

To test that native compilation is on, evaluate the following:

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

Conclusion

It's exciting to see both "pure" GTK and native compilation advancing the state of Emacs. This is exactly the spirit of FLOSS software that promotes ingenuity and advances the state of computing forward. A similar jump in performances hadn't happened since the 90's when byte-code compilation was added to Emacs.

Enjoy faster Emacsing!