Bringing GNU Emacs to Native Code (2020)
arxiv.org
arxiv.org
It's now been merged into trunk and it's going to be the default:
https://news.ycombinator.com/item?id=26935401
The only drawback I saw is that compiling Emacs itself takes 3x to 4x longer when compiling the native-comp branch.
Note: this was on an Intel mac; anyone with an M1 tried this yet?
I did run into some problems with some newer packages on the GNU ELPA. (Specifically consult, marginalia, and vertico by github.com/minad) Straight.el complained about not being able to find the packages. Any suggestions on what I might try to fix this?
brew install emacs-plus@28 --with-native-comp
might be a more standard "easy" approach on MacOS.brew tap d12frosted/emacs-plus
first? (I haven't tried it yet, and not sure I will.)
During editing/usage for sure: it is noticeable. Not that it was slow before but nearly everything now feels really snappy.
As for startup I don't know as I rarely relaunch it but I just tried (only for the native-comp branch):
time emacs -Q -eval '(kill-emacs)'
gives 160 ms. Or launching Emacs with -Q and then calling emacs-init-time gives basically the same (-Q bypasses the config files).Starting with my entire config which is quite beefy takes 1.2s. I could probably speed it up but haven't really looked into optimizing Emacs startup in a while.
I found it incredibly distracting. In Emacs... Magit is instantaneous while orgmode tangle/export is embarrassingly slow - but never a dealbreaker
anecdotally eshell has some of the lowest latencies as well https://danluu.com/term-latency/
Unfortunately sublime's plugin ecosystem isn't very lively.
I've been using it for a few months as well, and I haven't noticed any change. My start up time, as measured with 'emacs-init-time, is 0.6-0.7 seconds instead of 0.7-0.8, but that's about all I've noticed, and there hasn't been any difference in day-to-day editting.
I haven't had any problems, so that's good at least.
The first thing where it usually affected me was org-mode. It runs a lot of complex text-parsing and redisplay code on the buffers, and some frequently used operations (like folding/unfolding big sections of the file, or generating agenda) would noticeably slow down with files over a couple hundred lines. That problem is now mostly gone with nativecomp - probably in part because Org Mode itself is improving its performance, but in big part because all that Elisp just runs faster across the board.
LSP mode would probably be another common case - but I only started using it on nativecomp and with the new JSON parsing module (Emacs 27 started supporting libjansson in place of whatever it was doing before). It works very well, but I imagine it was pretty unusable before these two changes.
I'm not sure I understand what you mean there. Unless those third-party packages are scheduling tasks on the event loop then they should not be impacting performance of your other operations. Just having lots of things installed and activated doesn't affect performance.
Multiple installed emacs packages do not execute their code simultaneously unless they are using something like run-with-timer to schedule tasks on the event loop. Can you give me an example of an emacs package that causes custom elisp to be "running in your buffer" continuously?
For example, org-mode does many complicated things, but only when you ask it to -- i.e. when you issue one of its keybindings, or call one of its functions. Then, you enter a blocking ("synchronous") function call: the main thread is executing the org-mode code and nothing else until it's done. So, if things happen as I'm describing, you can see that it doesn't matter how many packages you have installed: only one of them is taxing the CPU at any one time, and they are not all queuing up work and causing the event loop to get bogged down.
Similarly, swiper, ivy, counsel, magit -- they only do things when you ask them to.
To put this another way: in general, your installed packages are not active simultaneously. Therefore, the performance of your editing operations is not affected by how many packages you use.
It's true that few things run in the background constantly. You can check for that easily, with M-x list-timers. On my work laptop, my most feature-packed Emacs instance, I currently have three timers active (and 11 waiting for a trigger). Those three are:
Repeat Function
5.0 auto-revert-buffers
60.0 ac-clear-variables-every-minute
3600.0 url-cookie-write-file
So there's only one frequently updating timer running, that was put there by (I think) lsp-mode.Of the inactive timers, there are a bunch on a really short trigger - for features like highlighting indentation guides, highlighting parentheses, etc. But these are the ones that wake up when you do just about anything in a relevant buffer - along with various hooks being fired.
Because it's the hooks that seem to be missing in your current understanding.
I just switched to a random buffer, and in it, I have 18 functions in post-command-hook, and 2 functions in post-self-insert-hook. That's 20 functions to execute on pretty much every single key press, and all of them come from optional features, both built-in and from additional packages. Syntax highlighting (font-lock-mode), error checking (flycheck), documentation helpers (eldoc), LSP stuff, snippet expansion, etc.
All these functions are usually fast (plenty of them just reschedule aforementioned inactive timers), but every now and then, one of them gets some heavier workload. And you notice a stutter. Perhaps inserting a character took more milliseconds than you're used to, because it caused font-lock to re-render the entire screen. Perhaps autocompletion triggered some expensive checks. Or perhaps there's just too many badly-written functions firing, and it added up to a noticeable delay.
For me, the three most common cases of annoying little delays were autocompletion (in Elisp, and Common Lisp via SLIME), font-lock and org mode folding. The latter two were an issue when working with large org mode files.
Native compilation improved Elisp performance across the board, and all but eliminated these issues for me. It won't help you if you put a badly written function in a frequently firing hook, but it does push some of the more expensive computations below the threshold of visibility, and makes it less likely that a lot of functions in a hook will add up to a noticeable delay.
1. I had it in my head that emacs arranged for font lock to be done in a separate thread? (Also, why is it called "lock"? I just use that word cos the emacs docs and code do)
2. Isn't org mode folding/unfolding just a blocking call that happens when you ask it to?
I'm not sure, but I don't think so. There's this thing called `jit-lock-mode' (check out the docstring of the function named jit-lock-mode for details) that makes Emacs fontify only visible parts of the buffer when triggered by redisplay code (in C core), + some extra fontification of the invisible parts on idle timer. But I don't believe it actually happens on a separate thread, given that redisplay can run arbitrary user code, e.g. through 'display property attached to a piece of text in a visible buffer.
> Also, why is it called "lock"? I just use that word cos the emacs docs and code do
So do I. I did a little googling, but couldn't find any definitive answer. Most likely explanation[0] seems to be that "lock" here means the fontification spec is attached to the text and updated automatically, vs. being refreshed globally on user request.
> Isn't org mode folding/unfolding just a blocking call that happens when you ask it to?
It is, but it's also something I do very frequently in quick succession. I'll usually press S-Tab in quick succession to perform global visibility cycling, because I often want to take a quick look at the outline of my file, and then get back to editing where I was. If it takes more than a fraction of a second, it's distracting for that use case.
--
[0] - https://old.reddit.com/r/emacs/comments/b3jsfc/what_does_loc...
However, font-lock-mode never executed in a separate preemptive thread.
524 57% - kill-all-local-variables
56 6% + magit-wip-after-save-mode-cmhh
56 6% + global-evil-quickscope-mode-cmhh
56 6% + global-eldoc-mode-cmhh
52 5% + global-page-break-lines-mode-cmhh
52 5% + global-font-lock-mode-cmhh
48 5% + smartparens-global-mode-cmhh
44 4% + global-prettify-symbols-mode-cmhh
40 4% + magit-auto-revert-mode-cmhh
40 4% + global-tree-sitter-mode-cmhh
40 4% + evil-mode-cmhh
40 4% + global-evil-collection-unimpaired-mode-cmhh
These are all third-party packages, have nothing to do with org-mode. Pretty much any globalized minor mode you enable will add some constant time to switching into fundamental-mode, and it all adds up.(Also, half the time when I run the profiler, the culprit ends up being evil-mode. Unfortunately it's so useful …)
As it turns out, these are automatically created by `define-globalized-minor-mode' macro - per docstring, it's used to define a global mode for buffer-local minor modes. I.e. that's how you get all these `global-XXX-mode` functions that turn on XXX-mode in every (relevant) buffer.
One of the things this macro does is create a `global-XXX-cmhh' function, and attaches it to `change-major-mode-hook', which is invoked by `kill-all-local-variables' whenever buffer's mode is changed. I imagine that, as part of generating org mode's agenda, files get opened and modes get switched a lot. Curiously, it also seems that (digging into `kill-all-local-variables' source in C), each call to it forces mode-line redisplay, so further computation may be caused by whatever is in your mode line (which can also be nontrivial). I haven't checked that though, maybe it redisplays mode line only once.
Thanks for bringing this up, it's another place where pretty much every mode stuff some hook, that I didn't know about :).
EDIT: curiously enough, I just profiled my agenda - both doing from scratch and redoing it - and `kill-all-local-variables' isn't even on its radar. I wonder what runs in your hooks (and/or modeline) then. The result from refreshing an existing agenda was roughly what I expected. Building it anew, after killing all open org buffers, was interesting. In my case, 69% of total time was spent opening files (`find-file-noselect'), before even doing any mode switching. In that, `projectile-find-file-hook-function' accounted for 50% of total agenda building time, half of that spent updating the mode line!
From what I see, it's just trying to determine the project to which a file belongs. I'll probably look for an option to make projectile stop being interested in files that aren't in the "known projects" subtrees without explicitly instructing it so.
Yes, that's accurate, but that's the whole problem. The code executes sequentially, so each custom execution adds latency. It's not unusual to have elisp from several packages executing with every keypress.
i use gnus and org-mode heavily. 80% of my day is in one or the other.
i would add that emacs28 w/o compilation feels faster than emacs27.
emacs28 w and wo compilation feel the same.
Small jit dependency. 1MB compared to 30MB.
Fast JIT. At least 3x faster than llvm.
All architectures. LLVM only supports a tiny amount of architectures, gccjit all.
Only the desperate do llvm jitting.
I'm using MacPorts and I have no idea how to hand compile just libgccjit there. The libgcc port doesn't even produce the jit language.
Macports has gccjit since gcc10, just somebody needs to update Emacs.
What others archs? All of course
I'm writing my own Portfile to get libgccjit 11 now. The emacs-devel port does bring in the entire gcc10, but I just want libgccjit.
port install emacs-devel +nativecomp
or port install emacs-app-devel +nativecompYou may try to report a bug[0] for Mojave but I wouldn't hold my breath.
I think that the next major leap for emacs needs to involve the garbage collector and allocation logic. I think that a large class of performance optimizations would be possible with an improved GC, including improvements to emacs existing threading capabilities.
Emacs is a single threaded synchronous and blocking UI system. Sometimes when my autocomplete, C++ checking, git checking, auto formating, ... run, the editor freezes, for multiple seconds.
All this stuff runs in the UI thread. Braindamaged.
The other thing that makes emacs slow is remote editing. Emacs TRAMP uses one ssh connection per command. VS Code spawns a remote server and asynchronously updates the remote's state. VS Code remote editing experience is as good as the local one, but emacs experience is supper laggy, recurrent freezes of multiple seconds, etc. Particularly when navigating the filesystem in the remote in any modern emacs way (helm, ido, etc.). Or when auto-save happens and everything blocks for multiple seconds, etc.
---
I don't really care if native compilation makes single threaded code faster, if that single threaded code runs in the UI thread and blocks the editor for 10 seconds. Sure now it maybe blocks for 9 seconds, because you can't do much about those 9 seconds you have to wait for some IO operation to complete. But that still sucks.
However I prefer Emacs anyday over VSCode, I am very productive in Emacs because I'm used to doing almost everything in it
You can open the same or different files in the different views, use one of the views for debugging, shells, remotely running tests, while you use the other views for other stuff (showing different files side-by-side, etc.).
I often see vim and vs code users doing a lot of shuffling around when working with multiple open files to switch back and forth between files or across tabs, but with emacs none of this is really necessary.
Btw the effects of MS funding can be overvalued - e.g. MsTeams is among the worst IMs in the existence.
ControlMaster auto
ControlPersist yes
ControlPath ~/.ssh/control/%C
Then run mkdir -p ~/.ssh/control/, if you haven’t already. Why this isn’t the default I don’t know, but once configured correctly you won’t have any problems from TRAMP.Everything that requires remote file system interaction is super slow. Emacs just seem to be doing lots of individual I/O operations.
VScode has a remote server that batches them before updating over the network.
The client sends operations to the server and queries the server for updates asynchronously. The server can perform multiple operations, and batch them into one response.
Stuff like, "regex on all files in this directory" is performed by emacs as "list all files in directory, wait, for each file, regex that file, wait". VScode just sends the "regex all files" and the server locally handles everything, and send one update back.
The difference is going from < 100ms for VSCode vs 5 seconds for emacs. Night and day.
The same happens for pretty much every modern feature (git status, diffs, blames and updates, autocompletion, correctness checks / intellisense, etc.).
I also use `helm`, `ido`, etc. to navigate the file system and they are all super slow.
Could it be a configuration issue in parts at least? Because it does start to sound like it. For example git status has never been slow for me in Emacs, except for huge diffs with lots of changes in lots of files. Same for diffs. I know, that autocompletion depends on the language and tools used for it and what things are checked for possible auto completion entries. It is possible to limit autocompletion to only use some sources, or to make it use a language server for some languages. When developing Rust, Python or TypeScript in Emacs, I did not experience slow correctness checks (I assume you mean type checks and unused variable kind of stuff.).
I searched a little and found the following blog, which claims, that there are issues with it, when you do heavy data transfers, for example via many rsyncs at the same time:
https://www.anchor.com.au/blog/2010/02/ssh-controlmaster-the...
I did not test it myself. This would seem like an appropriate reason to not make it the default.
I wonder if ssh shouldn’t have a sensible default like ~/.ssh/control/%C for ControlPath, so that you could just turn on ControlMaster and have it just work. Then TRAMP could set the ControlMaster option on the command line when it runs ssh. At least then people wouldn’t have to mess with their SSH config, and they wouldn’t have to consider whether it will break something else.
It is, it has been for a while. Check the TRAMP FAQ for details when it is being used automatically (grep for "ControlMaster"): https://www.gnu.org/software/emacs/manual/html_node/tramp/Fr...
Disentangling this is probably going to be difficult, but the Emacs folks are probably smart enough to come up with something that matches with the spirit of Emacs. Libuv and callbacks might be a way, but cooperative scheduling of user threads on backing threads together with structured concurrency, like Java's Project Loom is attempting, are another way.
The emergence of the LSP protocol is a very promising development in the architecture of editors and IDEs. Many things can and maybe should be passed off to background processes to handle, both to expand the feature set of every editor out there, and to increase speed and stability.
Is there anything in the communication of LSP making the LSP stand out from any other protocol? (I really mean the protocol, not the infrastructure on which it is used.) Other than that it is quite an old idea to have things at the ready running in a separate process. This time it is applied to editors, completion, type checking and other features. In general this simplifies making use of multi-core systems. Watch any Joe Armstrong talk about it. The question is, why it was not thought of before, or perhaps, if it was, then why it was not done before.
In terms of the protocol itself, I suspect it’s actually worse than a lot of other ones.
Choosing JSON to communicate certainly could have been more wisely decided. Moreover since sending entire file contents across for some queries, what a waste.
What really made the difference is that VS Code uses LSP to integrate languages. Instead of dozens of ad-hoc integrations, there is one unified way to integrate a language now, and Microsoft's backing ensured there is an ecosystem for it from day one.
Not all is rosy though, mind you. LSP uses JSON/RPC after all. Also, we are touching the Microsoft world here, which means that standard adherence is not necessarily a given[0].
[0] https://www.reddit.com/r/vim/comments/b3yzq4/a_lsp_client_ma...
Perhaps this needs be used more, or there is another mechanism to be developed allowing tasks to be interrupt/resumed in priority service to the editing functions. A key example may be mode line refresh: this certainly must happen on the main thread, but it ought not block other more important items.
Calling this "braindamaged" is hardly fair; I'm sure this decision was made ~35 years ago when it made more sense.
Emacs started as a terminal app, the GUI was added as an afterthought, by pretending it's a TTY. The concept of a "UI thread" wasn't on Stallman's mind back then. It continued to evolve from there; fast forward 35 years, and now we're living with a GUI program that still thinks it's writing to a teletype[1].
--
[0] - https://stackoverflow.com/questions/10084842/first-gui-versi...
[1] - https://m.facebook.com/nt/screen/?params=%7B%22note_id%22%3A...
They would've been wildly forward-looking if they had figured this out 35 years ago though.
For Nix users on macOS, see[1]. Here's how I used it in my home-manager config[2]. For Nix users on Linux and NixOS, see the Emacs overlay[3]. There's even Emacs nativecomp + wayland support (emacsPgtkGcc) there.
[0] https://akrl.sdf.org/gccemacs.html
[1] https://github.com/twlz0ne/nix-gccemacs-darwin
[2] https://github.com/siraben/dotfiles/blob/8161e1b72965b48f822...
And here are the slides: https://www.european-lisp-symposium.org/static/2020/corallo-...
Recently, I have switched to Emacs full-time (first spacemacs[0], now doom[1]). So far, it has been a great experience. Spacemacs is a bit slow but with doom and `native-comp`, I rarely encounter any performance issues.
Bringing GNU Emacs to native code [video] - https://news.ycombinator.com/item?id=23066971 - May 2020 (83 comments)
This might entice me to build it and try out my Emacs Doom setup again.
My usage model is such that I only start one Emacs and use emacsclient to add files for editing from terminals. Emacs is running all the time (weeks/months).
Since Emacs is my primary interface to my Linux box, I give it some priviledges. In the .xsession I do:
vmtouch -t $HOME/.emacs.d; vmtouch -ld /usr/bin/emacs-gtk
Which effectively ensures that critical Emacs images and data is present in the memory all the time. Check out vmtouch utility. It might be useful for other purposes as well.
I need my editor to "feel" really smooth and instant during editing. Emacs just never gives me that experience.
I think many long-time Emacs (and Jetbrains IDEs, for that matter) users just don't notice how laggy it is because they are so used to it, or are not very latency sensitive.
Does it fix having to restart Emacs after at most 8-16 hours of use?
Could this experience be a plugin I use, or my own idiocy? Originally, I assumed "YES! Of course! I am idiot... This is my fault!" Then came the observation when pairing with people (across a variety of languages) who use Emacs, who all say on a regular basis, without so much as a thought to it:
"one sec, I just need to restart Emacs."
Followed by 30 seconds of restarting Emacs and 30 more seconds setting up the buffers. I'd definitely use Emacs but the inevitable, and uncontrollable, lockups kill it for me.
FWIW, my Emacs init time (run `emacs-init-time`) is between 2–7 seconds, and I'm not doing anything particularly special to get that working.
I'd start out with a fresh .emacs config file and then add the packages you want with the excellent `use-package` macro, setting `:defer t` as much as possible. (Often this will be implicit if you have `:mode`, `:hook`, or `:bind` configured, if I'm not mistaken.)
It would be better if Emacs were less sensitive to how your config is setup, but alas, we live in a fallen world. You can also ask around on r/emacs and get some better tips than mine.
Actually, I just checked my init time with that command, and it's ~2.5 seconds. I wouldn't call my config heavyweight, but it does have evil, lsp, vterm, ivy, magic, etc. I am using the native-comp branch though.
Considering how lots of people prefer to start Emacs once, preferably as a server (maybe even as a systemd user-unit!), and keep it running forever, not even closing buffers, until eventually the machine must be rebooted for whatever reason...
I would have to say yes.
https://lists.gnu.org/archive/html/emacs-devel/2008-12/msg00...
It is a major attraction of Emacs that its inspection facilities can be easily used to automatically analyze such issues.
My expectation of Emacs is that I never have to restart it. If it crashes or fails to reliably reclaim memory, please consider filing an issue with M-x report-emacs-bug RET.
[1] https://200ok.ch/posts/2020-10-01_introduction_to_profiling_...
About the only issue I have with this setup is that I can't get color emojis to work using Microsoft's Segoe UI Emoji :). That, and if I want to call out to some Windows executable, I have to ensure I use :connection-type 'pipe instead of (the default) TTY in make-process, because otherwise I get a disconnected process that can't communicate with Emacs. But the latter is a very rare case (I only need it to drive the build system I use at work from Emacs, because it lives Windows-side).
Startup time with nativecomp? Typically around 5 seconds on Linux, 15 seconds on WSL. If I happened to update a lot of packages before restarting (as I sometimes do, I like to clean out runtime state when I do bulk updates), then on WSL the startup goes up to about 1-2 minutes - that is, Emacs is usable in about 30 seconds, but async native compilation keeps pinning most CPU cores, so I just let it work in peace for that extra minute.
Emacs has many solutions to restore buffers. I use `desktop`, which is built-in. Use whichever you like, I really can't tell you the pros/cons.
Have you ever tried setting the garbage collection treshold to a large value? There's definitely no use for it during startup. It might make sense though to trigger it in a hook after startup.
You can try evaluating this in the scratch buffer
(if (member 'nativecomp features)
(message "yay")
(message "nay"))
Or check if a non-interactive function called `native-compile` exists.Or open help for an elisp function (next-line, for example):
c-h f next-line RET
Gives me: next-line is an interactive native compiled Lisp function in ‘simple.el’.
Or, m-x disassemble next-line RETAt this stage nativecomp was not yet finished, see his webpage for the progress. Expect another paper for the final evaluation of the improvements. It has big impact on programming languages implementations, esp. compared to llvm.
It's the very first big gccjit project, and a huge success.
[1] https://sigdoc.acm.org/conference/2017/guidelines-experience...