emacs 转 rust 的又一波冲击

Design of Emacs in Rust

R ust UN der E macs

祝愿他能成功 :wave::smile:

1 个赞

在大家追流行语言浪费时间前,请阅读:Re: Consideration for Rust contributions in Emacs

不管你用什么语言实现,只要你不动脑筋,不可能把一个单线程应用改的能 preemptively multitask。比如说,Emacs 中常见有以下规律:

  Lisp_Object tem, foo;

  for (tem = list; CONSP (tem); tem = XCDR (tem))
    {
      CHECK_STRING (XCAR (tem));
      foo = XCAR (tem);
      bar (SSDATA (foo));

假设有两个线程同时改变 list,rust 编译器最多会强制你在 CONSP,和 XCDR 之间加上 type check 和 lock,这样不仅慢,还容易导致其他错误:

thread 1: CHECK_STRING (XCAR (tem));

thread 2: XSETCAR (tem, make_fixnum (0));

thread 1: SSDATA (XCAR (tem))

此时,tem 已经成 fixnum,而不是 string。Rust 编译器不可能检测出这种问题,因为里面确实没有实现语言级别的 data race。

还有就是,wait_reading_process_output 究竟怎么处理 SIGCHLD?process filter 在那个线程中运行?

不管你用 C 实现还是用某种流行的语言实现,要做的工作还是一样的,而用其他语言实现不但会伤害 Emacs 的可搬运性,还会强迫核心开发者学习那个流行语言,造成社区分裂等问题。

21 个赞

没仔细看过,只是单纯的希望emacs能多些改进吧。感觉很多痛点存在了很多年,就现有的管理方式要有大的改变有点太慢太难。也希望有个neovim相对于vim的角色出现,愿望是好的吧。看他写过这篇: A vision of a multi-threaded Emacs,反正有总比没有好。

neovim刚出现的时候很多人也说社区分裂,可是自然发展的规律里也没有说一定不能分裂吧,也可能分裂后再merge的。比如git的分支策略

而 neovim 至今都没有做到多线程运行,基本上都是 subprocess 中,或由其他语言的 runtime 提供,不能从单独的线程中调用 neovim API。。。

另外,neovim 开始可没用 “rust = 好” 或者 “换个编译器,就能多线程” 这种不科学思路开发,neovim 的理想比这个人的主意实际多了。

1 个赞

之前的那一波是指 remacs 吗,现在好像停止开发了

我就是聊着玩,看到有人做新的尝试就高兴,不管是什么语言,也不一定要是RUST。感觉现有的emacs中陈旧的包袱太重了,需要一次大的变革,但是感觉社区的管理模式又不允许。所以寄希望有外来因素吧,也许等的次数多了也许真的就有人成功了呢 :smile:

neovim 虽然没有解决所有问题,但是它也是近些年促进vim发展的最大动力,至少vim可以脱离vimL了

3 个赞

我写过不少 Rust 项目,不得不承认 Rust 确实是一门设计良好的工程语言,但我个人是比较反感什么项目都拿 Rust 重写一遍的风气,尤其是作为编辑器的 Emacs 这种需要维护各种可变状态的应用程序,我不认为用 Rust 加一堆 RefCell Mutex 或者用一堆 Unsafe 来躲过编译器检查后,性能和可维护性能比 C 好到哪里去。

4 个赞

多线程的关键是要分开 “图形线程” 和 “非图形线程”, 图形线程只做常量界面绘制, 非图形线程可以一直在后台跑, 不管是短期还是长期跑, 只要不影响图形线程就不会导致卡界面或卡手。

不管什么语言, 都可以很好的实现多线程模型, 这一点我同意 oldosfan 的观点, 多线程的关键是线程代码之间不要同时操作一个对象, 特别是图形对象, 和什么语言实现没有关系, rust自成一体系生命周期第一不能解决Emacs的多线程问题(注意不是多线程,而是Emacs的多线程), 第二, Emacs这种富应用场景的软件其实更适合GC,而不适合Rust, 太束手束脚了。

当然, 楼主的目标是谈性能问题, 我个人的经验, 分享怎么解决性能问题:

  1. 外部进程多线程程序来支援: 用类似EAF或者lsp-bridge 的技术, 首先把所有耗费性能的代码先分离到外部进程中, 再在外部进程中有其他语言(不一定是Python, Golang, Rust, TypeScript等都可以)自身完善的多线程支持来实现 “非图形线程” 模型, 最大程度减轻 “非图形代码” 对Emacs的性能拖累, 外部进程计算完结果后, 通过IPC的方式把计算结果发送给Emacs, 由Emacs本身来渲染图形。 这样的模型的好处是, 各种语言的多线程都很成熟, 马上就可以编写代码来实现非图形部分的算法, 因为进程隔离, Emacs本身除了绘制界面啥活都不干, 所以Emacs很流畅。 缺点是, 有一些顽固的Emacser就是卡死也不用非 lisp 的语言写代码。 我期待有一天大家把 lsp-bridge 类似的IPC技术用于桥接到 Common Lisp 或者 Scala 类似的lisp 风格语言, 会大大减轻Emacser 的内心洁癖。

  2. 两个Emacs实例的方法: 类似第一个方法, 但是是一个Emacs 用于绘制,第二个Emacs只用于执行 Elisp 计算代码(比如做文本搜索, 列表计算等), 第二个Emacs计算结果以后, 通过IPC发送结果给第一个Emacs进行绘制。 这样的好处满足心中的洁癖, 两个进程都在写Elisp代码。 缺点是, Elisp本身没有多线程, 每开一个耗时计算任务都要启动一个新的Emacs, 而且一定要注意新开的Emacs只能做Elisp本身的计算或者subprocess filter计算, 不能被第一个Emacs的配置所污染了。 同时, 如果IPC就是追求性能, 其他语言的绝对执行性能都要比Elisp好很多。

  3. 平行宇宙改造: Emacs自身的多线程是RMS写Emacs的时候根本就没有考虑多线程(包括vim也是), 导致几十年累计的代码就像面条一样绕在一起, 没有分 ”图形API“ 和 ”非图形的语言API“, 而Emacs很多插件本质是在做光标和Buffer操作代码, 当多个线程同时操作这些图形API时就会导致很多锁冲突, 也就是现在Emacs这种类协程的设计没法解决很多CPU密集性场景。 解决方法很简单, 就是不要动现在这些已有API, 因为API太多了, 你动一部分根本就解决不了多线程锁的问题, 而且还把现在的API弄坏, 已有插件跑不起来, 失去群众的支持。 需要创造新的API, 这些API在设计的时候就要考虑是图形API还是非图形API, 图形API限制在主线程运行, 非图形API跑多少个线程都无所谓, 只要不要在子线程运行图形API就好了, 这样的设计和Gtk、 Qt是一致的, C语言本身就支持多线程。 这些新API就像一个平行宇宙一样, 和原来API互不干扰, 所有多线程插件用新的API去构建。 缺点是, 工作量浩大, 不是一个人可以完成的, 需要巨大的力量投入, 还要说服Emacs社区合并新的API, 非常容易因为力量不够或者不懂多线程的喷子乱喷, 而半途而废。

  4. GC改造: 在我编写 lsp-bridge 的过程中, 我发现Emacs的性能主要是多线程和GC, 多线程隔离计算上面已经说了很多了, 我们只是专注于Emacs主线程的绘制性能来说, 单单只是绘制对象, 比如 buffer text, highlight, overlay, 如果只要是常量绘制, Emacs的性能还是够的。 现在的问题是Emacs的GC太差了, 只要创建上千个对象, GC就会频繁介入, 而且GC运行效率太差, 导致对象多了GC就会导致主线程卡顿, 这一点不解决, 就算第三点解决了, 还是会导致主线程卡顿。


关于GC, 我自己有一些想法, 可能不对, 先抛砖引玉给大家思考:

  1. GC对象回收的性能要做极大优化, 这一部分代码的性能提升比native comp 这种投入产出比高很多, 效果也明显很多, 改善以后基本所有插件都会受益

  2. GC对象看看能否快速 dump 出去, 用外部进程去分析哪些对象是否需要回收, 这样可以把非常耗时的分析工作分离到外部进程中去做, 免得分析一下GC对象都会卡死Emacs

  3. GC默认的策略代码改一下, 现在都是大内存时代, 不要动不动就那么敏感, GC可以慢慢回收, 不要求一次回收所有不用对象, 通过第二步的迭代, 依次慢慢回收, 但是保证每次回收Emacs主线程消耗不要超过一定时间, 比如 5ms, 超过这个时间就下次再回收


上面是我这几年对Emacs性能的实践和一些想法分享给大家, 我个人觉得现阶段最有必要的反而是改造GC, 效果就会明显。 也希望大家讨论Emacs多线程的时候, 多思考多线程的本质, 把思路理清楚再讨论就会相互有启发, 相互可以学习。

我个人认为如果不好好分析现状, 只是期望Rust这种语言能够解救Emacs的诸多问题是非常不现实的, 因为不考虑 ”经济可行性“ 或 ”Emacs可持续性发展” 的前提去讨论怎么优化Emacs性能, 其实除了浪费时间和无谓的争吵, 并没啥用。

34 个赞

有现成的还不错的gc实现,比如老传统bdw-gc,ravenbrook有一个叫mps的gc也适合emacs使用,但是!我觉得 emacs 现在一个很大的问题就是 C 代码写得太💩了,新开发者看了那坨 C 代码都头痛半天,比如一个 malloc 就有 xmalloc,lmalloc,lisp_malloc 好几个函数/宏,更别说显示部分一个 xdisp.c 单文件几千行,单函数几百上千行。没有深入的理解想修改 emacs 的 C 代码太痛苦了,所以才会有不少人老是想着用 Rust 重构 emacs,不一定是单纯追求潮流。这些本质是 emacs 十几年维护叠起来的💩山,俗话说『代码还能动,就不要管他』……

9 个赞

最主要还是设计和策略的问题, 方向对了就好了。

至于代码细节, 那个可以慢慢重构。

1 个赞

应该有了, https://www.reddit.com/r/neovim/comments/t2oc7w/neovim_now_supports_true_threads_via_libuv/ 在 neovim doc里有写:

Plugins can perform work in separate (os-level) threads using the threading
APIs in luv, for instance `vim.loop.new_thread`. Note that every thread
gets its own separate lua interpreter state, with no access to lua globals
in the main thread. Neither can the state of the editor (buffers, windows,
etc) be directly accessed from threads.

A subset of the `vim.*` API is available in threads. This includes:

- `vim.loop` with a separate event loop per thread.
- `vim.mpack` and `vim.json` (useful for serializing messages between threads)
- `require` in threads can use lua packages from the global |package.path|
- `print()` and `vim.inspect`
- `vim.diff`
- most utility functions in `vim.*` for working with pure lua values
  like `vim.split`, `vim.tbl_*`, `vim.list_*`, and so on.
- `vim.is_thread()` returns true from a non-main thread.

oldsfan 其实说的也基本上是对的,neovim 的线程里能看得到的都是 lua api 的东西,老的 vim 的 api 的东西都是看不到的,尤其是 buffer window 有关的都是看不到的。

这个是我上面说的第二种方案, 基本上就是两个 Lua, 在两个Lua之间传递数据。

Lua语言本身是没有多线程的, 所以 Neovim 也只能用这种 “进程模拟线程“ 的方式。

对的,但是Lua快呀

这是类似js的webworkers方式了

对, Lua超级快, 而且也没有Emacs的GC负担, 所以给大家感觉, 即使没有多线程, Neovim都足够快了。

好像luaji的gc就才不到一千行?LuaJIT/lj_gc.c at v2.1 · LuaJIT/LuaJIT · GitHub

真是小而美 emacs触发GC的时候真的是什么都做不了。MELPA上甚至还有这样的包 Koral / gcmh · GitLab

Lua 好像被用作魔兽世界游戏的引擎开发

还有不少游戏用lua做脚本,10多年前一直玩的海盗王也有lua脚本

我在2016年开始自学编程那会,lua 不要说中文资料,就连英文资料都没有很多,甚至跑到官网连个面向新手的教学都找不到。

反倒是 lisp 入门的书不要太多。

2 个赞