Emacs Native Compilation: A Practical Engineering Decision for Elisp Performance
Emacs 28+ native compilation translates byte-compiled Elisp to machine code via libgccjit. This blog covers the problem it solves, how to enable it, a concrete benchmark showing 2× speedup on a regexp-heavy helper, and the trade-offs: larger cache, first-load latency, and dynamic-code fallbacks.
11 Oct 2026, 06:59 UTC

The Problem: Interpreted Elisp at Scale
Large Emacs configurations — dozens of packages, thousands of lines of custom Elisp — often feel sluggish on startup and during interactive use. The culprit is the Emacs Lisp interpreter: every function call, macro expansion, and loop iteration pays interpreter overhead. Byte-compilation (.elc) helps by pre-parsing and optimizing, but the byte-code VM still dispatches each instruction. For hot paths like regexp-heavy search helpers, project-wide grep, or complex mode hooks, that overhead adds up.
What Native Compilation Changes
Starting with Emacs 28, the native compiler translates byte-compiled Elisp into native machine code via GCC's libgccjit. The pipeline is:
- Source
.el→ byte-compile →.elc(unchanged workflow). - On first load, the native compiler reads the
.elc, emits C-like IR, and invokeslibgccjitto produce a shared object.elnstored ineln-cache/. - Subsequent loads map the
.elndirectly, bypassing the byte-code VM for compiled functions.
No source changes required. The compiler is selective: it skips code that uses dynamic features (eval, funcall on symbols, dynamic scoping) and falls back to the interpreter for those forms.
Enabling It in Practice
You need an Emacs built with --with-native-compilation (GCC + libgccjit-devel / libgccjit-10-dev etc.). Most distro packages (Debian emacs-gtk-nativecomp, Fedora emacs-nativecomp, Homebrew emacs-plus@30 --with-native-comp) include it. Verify:
M-x emacs-version
;; look for "native-compilation" in the features list
(native-comp-available-p) ; => t if supported
Then in early-init.el (before package loading):
(setq native-comp-eln-load-path (list (expand-file-name "eln-cache" user-emacs-directory)))
(setq native-comp-async-jobs 4) ; parallel compilation jobs
(setq native-comp-deferred-compilation t) ; compile after startup
(setq native-comp-async-report-warnings-max 10)
Ensure the cache directory is writable. On first run, Emacs will populate eln-cache/ with .eln files for every loaded .elc.
Worked Example: Benchmarking a Hot Helper
Consider a project-grep helper that runs a regexp over many buffers — a typical hot path:
(defun my/project-grep (regexp)
"Return matches for REGEXP across project files."
(let ((matches '()))
(dolist (file (project-files (project-current)))
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward regexp nil t)
(push (list file (match-string 0) (line-number-at-pos)) matches))))
(nreverse matches)))
Benchmark before native compilation (clear cache first):
M-x benchmark-run
;; or in *scratch*:
(require 'benchmark)
(benchmark-run 10 (my/project-grep "defun"))
Typical output on a medium project (≈2k files):
| Run | Elapsed (s) | GC (s) |
|---|---|---|
| Byte-compiled only | 4.8 | 0.6 |
| After native compilation | 2.1 | 0.3 |
The speedup comes from eliminating VM dispatch inside the tight while loop and re-search-forward calls. Source code is identical; only the .eln changed.
Trade-offs and Limitations
- Disk & memory:
.elnfiles are larger than.elc(often 3–5×). A 200-package config can add 200–500 MB ineln-cache/. - First-load latency: JIT compilation happens on demand. First startup after cache clear can be 2–3× slower while
libgccjitcompiles each module. - Dynamic code falls back: Heavy use of
eval,applyon runtime symbols, orlexical-binding: nilcode prevents optimization. Those functions stay interpreted. - Cache invalidation:
.elnfiles are keyed by Emacs version,load-path, and source checksum. Upgrading Emacs or moving packages requiresrm -rf eln-cache/orM-x native-comp-clear-cache. - Platform gaps: Windows native compilation is experimental (MSVC + libgccjit port). macOS works but requires Xcode command-line tools for GCC.
Verification Checklist
- Confirm support:
(native-comp-available-p)returnst. - After loading a package, check
eln-cache/for corresponding.elnfiles. - Run a representative benchmark (like the grep helper) before and after
native-comp-clear-cache+ restart. - Watch
*Messages*for "Native compilation" warnings — they indicate fallback to interpreter.
Actionable Closing
Measure your baseline with emacs --batch -l benchmark.el -f benchmark-run …. Enable native compilation, clear the cache, and re-measure. Keep performance-critical code byte-compiled (avoid eval in hot loops) and prefer lexical binding. The gains are real for typical interactive workloads, but they come with cache management overhead — treat eln-cache/ as a build artifact you can delete anytime.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.