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
,重新执行编译脚本