优化windows版本emacs的总结(持续更新 2024-2-16)

ps:之后会把相关优化的研究统一放在这个帖子


windows下emacs为什么会慢(这点也希望大家多给一些反馈,可以针对性的研究)

1 默认的子进程数量太小,
之前发了这个帖子 关于windows下emacs报错Creating process pipe: Too many open files的真正解决方法 但是帖子里的说法有问题,见2,
由于模拟机制里面用到了 WaitForMultipleObjects ,它受限于 MAXIMUM_WAIT_OBJECTS 的值,不能超过64,所以子进程数量不能超过32。想处理这个问题,只能从模拟机制入手。
在msdn的链接里提到了解决办法,可以创建一个线程来等待 MAXIMUM_WAIT_OBJECTS 句柄,然后等待该线程和其他句柄 WaitForMultipleObjects function (synchapi.h) - Win32 apps | Microsoft Learn

2 创建子进程时,windows需要额外的模拟机制。
看了下src/w32proc.h,在885行附近有一大段注释,可能和这个问题有关。
里面提到emacs中,启动子进程后,会启动一个读线程,去监视子进程的输出和状态。
944行附近的注释提到emacs希望pselect对于所有的文件描述符都起作用,包括管道等。但是windows的select只支持套接字。所以需要通过另外的机制模拟。这可能就造成了windows上启动子进程慢。
这个文件貌似是1993年写的,后面就没有大的更新,但是windows后面还是有很多新的机制,可以研究下是否有替代的办法。比如IOCP?
在另一篇帖子 使用 lsp-pyright ,提示 too many open files, 大家有没有遇到过这种情况? - #11,来自 jiacai2050 里有提到类似的操作,用poll去替代emacs现在用的select。可以参考下它的处理方式。

3 可能是msys2的锅,详见这个issue Is it possible to increase program execution speed by caching the result of path conversion · Issue #193 · msys2/msys2-runtime · GitHub
pacman在同步仓库时,在短时间内就进行了几千次的路径转换,估计emacs也类似。这个问题就不好解决了,能想到的一种办法是把temacs迁移为 native windows程序,不过这个工作量着实有点大

4 windows进程启动速度本身慢 (2024-2-16更新)
随便写一个简单的程序,分别编译到win和linux上,再用CreateProcess和execl执行500次,windows上需要1800毫秒,linux上只需要500毫秒。
方案:能想到的有两个,1 使用进程镂空技术,创建镂空进程池,不过这个比较黑科技,稳定性不一定很好。2 尝试直接调用ntdll里面的API,相比去调CreateProcess,性能肯定要好一些,但是好多少说不准,得尝试一下,但是也有稳定性问题,不一定所有windows版本都能兼容。
今天看了下w32heap.c的源码,发现个有意思的东西,它里面也是直接用的ntdll的函数,RtlCreateHeap,里面提到了原因是因为普通的HeapCreate函数不能在指定的地址分配堆内存。昨天尝试了RtlCreateUserProcess函数,创建进程的速度可以达到linux差不多的速度。后面再看看能不能稳定使用。

最新的研究见这个帖子,最简化的进程启动代码也慢很多,这块看来没啥优化空间了

5 emacs源码里有些地方时用了ANSI版本的API
看emacs源码发现了这个问题,很多字符串和API都是用的ANSI版本,windows上用UNICODE版本理论上来说也会有性能的提升。

6 编码问题
w32.c 中1570行提到emacs默认是把所有文件名当作utf8编码,但是我们平时用的windows基本都是用的本地编码,比如gbk,当操作大量文件时,需要做额外的编码转换,这可能也是慢的一个原因

7 emacs在win上的内存管理可能不够高效(2024-2-8 更新)
调试跟踪会发现内存的分配和释放最终会进入到w32heap.c里面去,这里面直接使用了windows的一些api,比如HeapAlloc HeapFree之类的函数。
之前我尝试过使用mimalloc劫持ucrt的内存分配操作来加速, mimalloc结合emacs确实提升了windows上的使用体验(应该没啥用) 现在来看明显不行。
它不用CRT的内存操作函数。
相关的内容较多,放在这个贴子 windows版emacs内存管理分析
尝试了用mimalloc替代w32heap.c里面的mmap_函数,测试发现没有性能提升。

8 cmdproxy的存在(2024-2-6更新,已解决,详见 https://emacs-china.org/t/windows/26577)
为了抹平平台的差异,windows上额外引入了cmdproxy用来执行命令,cmdproxy.c最后编译出来是一个单独的cmdproxy.exe。不过具体哪些情况用到了这个程序还有待研究,看是否存在优化空间。


优化措施

1 启用各种常见的编译优化指令

CFLAGS='-march=native -Ofast -fno-finite-math-only -fprofile-generate=/c/opt/profile_data' ./configure --without-dbus --enable-link-time-optimization --with-native-compilation=yes

这里编译参数其实是比较有争议,不过我本地使用下来没报错,开着也影响不大
这里稍微解释下,
-march=native 是针对本地CPU启用AVX。编译器会尽量使用SIMD指令。
--enable-link-time-optimization 启用链接时优化

2 启用PGO优化
这个优化效果立竿见影,编译速度相比默认的情况都会快2分钟左右(因为emacs本身的构建过程就是先把c代码编译成一个temacs,再用它去执行elisp代码)
缺点是比较麻烦,得编译两次,
第一次得用上面的指令编译,第二次得把其中的profile-generate换成profile-use
它的原理就是第一次编译之后,运行程序,会记录执行的路径(说法不一定准确),第二次编译会根据执行的情况针对性的优化。

3 优化cmdproxy和magit启动速度
见这个帖子 关于cmdproxy的优化和magit的优化


总结编译操作步骤:
1 clone编译脚本到本地 GitHub - Eason0210/build-emacs: Shell scripts for build GNU/Emacs
2 clone emacs 源码git clone https://github.com/emacs-mirror/emacs~/src/emacs
3 git log记录下最新提交的hash
4 修改编译脚本源码,把198行左右的两个configure命令换成上面优化步骤1提到的命令
5 进入msys2环境,执行编译脚本./build-emacs.sh 上面记录的hash
6 执行之后打开runemacs.exe。随便执行一些操作,我这里是打开了~/tmp/emacs-build/lisp/*.el~/tmp/emacs-build/src/*.c,然后关闭emacs
7 修改编译脚本,把configure命令中的profile-generate换成profile-use,重新执行编译脚本

18 个赞

既然

或者 -O3 更快, 那为啥 FTP 上的预编译都是 -O2?

gcc编译参数的详细说明可以参考gentoo的文档 GCC optimization - Gentoo wiki 和这个 https://thorium.rocks/

-O3相比于-O2,可能对性能没有明显提升,但是因为要开启AVX,所以需要用3
而且开启了新版本的AVX支持,在老机器上就没法运行,所以厂商给的版本一般都不会启用最新的AVX指令

2 个赞

Windows emacs 慢的根源是 Emacs 开发者一般用 linux,没有动力优化 Windows

对呀,所以自己上手。只要能调试,再复杂的代码也能自己捣鼓

1 个赞

我在Windows 上使用Emacs最大的障碍是git太慢了 :joy:

1 个赞

这个只能有64子进程(实际是32个)是靠什么解决的?这个问题对于我这种默认开 lsp 的比较难受,过个一两天进程数量就爆了。
根据 w32proc.h 中的说法是因为 Windows 的 WaitForMultipleObjects 这个 api 只支持监控64个子进程,现在包括 zsh 在 Windows 下也像 Emacs 一样只能有 32个子进程。 所以是怎么解决的呢?

要是论坛有个 wiki 页面就好了,这种帖子长了之后每次都要来回翻很久总结

2 个赞

我都习惯了win上magit操作一下喝口水了,急的时候还要换别的工具

1 个赞

你说的对,我上面理解有误,通过WaitForMultipleObjects模拟的情况,只改FD_SETSIZE是没用的。这个问题可能得从模拟机制入手。我前面修改了FD_SETSIZE的值,不会报错,但是应该是没有正常生效

翻了下msdn的文档,应该是可以解决的,它提到了两种解决办法。
一种比较直观的是“创建一个线程来等待 MAXIMUM_WAIT_OBJECTS 句柄,然后等待该线程和其他句柄”
我研究下,链接如下

这个“理论上来说”靠不靠谱?有官方文档或者微软博客上有这么提到过吗?W 的函数性能优于 A 函数?

git还行,慢的是magit,我现在都基本都是vc-dir了,vc功能不够的才用magit

因为nt时代之后,windows内核都是用的UNICODE版本的字符串,就算在用户态用ANSI的API和字符串,传到内核之后还会做转换

:joy: 我习惯了在win上用magit时操作一下眼睛就离开屏幕休息下。

1 个赞

git for Windows本身就慢,虽然magit也很慢。我现在都直接用WSL了,快到起飞

不能忍, 体验差距太大.

另外, 前几年emacs在mac上magit挺流畅, 跟linux差不多, 后来新版本的macos也不行了(好像是不支持vfork函数了), magit速度跟windows差不多, 让我放弃了mac.

我的情況是, 休息太多次會覺得很煩燥… :thinking: :joy:

1 个赞

我开始也烦。 Magit在Windows下的体验估计要等到集成了libgit2才会有改善了。

那個不是已經宣告失敗了嗎? :thinking: 我之前有用過, 但是效能沒有太大提升.