emacs 转 rust 的又一波冲击

总的来说这个帖子的内容还是非常有价值的,我们可以体会到Emacs的核心开发者在做某些重大决策时的权衡角度。

目前用rust重写emacs的全部以失败告终,生态太重要了

要成功肯定得兼容绝大多数现有的elisp代码,不然等于完全另起炉灶新做一个编辑器了 :joy:

强如neovim到现在也依然兼容老的vimscript,我自己以前配的config到现在还是老的vim脚本和新的lua混用的。我理解这种模式比较符合懒猫大佬说的平行宇宙改造方式,老的api不要动,新的改进做新的api。新的api如果能带来实际的好处,围绕新api的生态自然会慢慢建立起来,在新的生态成熟以前老的插件都可以正常用。

另外采用这种模式的话是用c还是用rust我觉得也不是最重要的问题了,完全可以看愿意做这个事情的开发者自己熟悉和愿意用什么。(而且我觉得如果要合并rust的code的话这种模式是唯一还有一点可能说服社区的,至少加入的都是新的模块,可以和老的c代码大体上隔离开。其他模式如果妄图直接用rust去动老的c代码的API,感觉社区接受的可能性为0 :smile:

当然了嘴上说说都很容易,实际做起来就难了 :joy:

感觉编辑器跟网络浏览器的工作模型比较像, 都是用户交互比较频繁. 浏览器的一些优化思路可以参考, web worker是个真正并发运行的东西, 它好像是创建了一个独立的js运行环境来运行worker, worker可以真正并发, 其开发语言也是js.

有人说可以启动一个emacs子进程来实现类似worker, 不过线程多了就要启动很多个子进程, 代价较大.

web worker的内部实现方法不太了解, 个人猜测好像浏览器里面创建了两个独立的js环境, 两者之间隔离, 提供专门的接口让他们通信. 比如在主js环境中, 有代码创建了个worker, 然后worker就会被放在第二个js环境中运行, 运行后的结果, 通过专门接口将数据返回到主worker, 对主worker来说类似于异步事件.

针对emacs, 是否可以考虑再创建一个额外的elisp解释器? 此解释器与现有解释器隔离开, 比如不能访问界面, 不能交互, 不能访问全局变量, 专门用来运行所有的worker, 真正的并发运行. 两者之间有接口可以互传数据, 此数据最好是传指针, 不要拷贝大量数据.

如果这样, 现有的elisp代码很多可能没法直接运行在worker环境里, 不过可以慢慢积累新的代码, 不知道emacs用户是否可以接受.

其实我最近想了一个更简单的解决方案. 解决 Emacs 的问题似乎不需要多线程, 单线程就可以, 但是 Elisp VM 必须是可以中断且恢复的. 比如

;; 占用 10 分钟甚至 9 分钟的超长卡顿函数
(heavy-consuming-job)

目前 Emacs VM 是可以被用户事件中断的, while-no-input 宏就是一个很好的例子, 用 while-no-input 宏包裹长时间运行的函数, 随便按一个键就能恢复响应, 但是目前的设计下, 那个运行了一半被中断的函数的现场就被丢弃, 无法重新恢复了.

如果 Emacs VM 可以保存上下文(这应该很简单, 先保存 Elisp VM 的 call stack, 再保存 C 的 call stack, 有大把 C 协程库这么做). 那么耗时函数就可以被用户打断为多个小任务, 然后在一个系统线程上调度运行, 这样也不需要多线程程序的加锁. 看起来像 preemptive scheduling, 其实是 user event guided co-operative scheduling,

用伪代码表示一下调度过程, 实际实现可以直接用 C 来调度, 也可以加入保证某个任务必须运行多少时间片, 才会被用户的 key event 打断的逻辑

(defvar *waiting-jobs* '())

     (while-no-input
         (heavy-time-consuming-jobs)
       (lambda (rest)
         (push rest *waiting-jobs*)))

     (run-with-idle-timer 0.1 t #'execute-waiting-jobs)

引入协程还有什么好处呢? 就是简化异步 API 的运用, 目前 Emacs 的异步程序 API 还比较复杂, 要设置 sentinel 和 filter 才能获取异步程序的结果, 代码逻辑都写在了 callback 里面. 而有了协程, 异步程序就像同步程序一样简单.

现在回想起来, Emacs 26 加入的 thread 其实是搞错了方向, 因为它既然是协作式调度的, 但又必须用户提供静态的 yield point, 不符合 Emacs 实际情况 (按用户事件作为调度点更好). 如果 yield point 只能是静态的, 那么和 generator.el 又有什么区别呢 :rofl:

8 个赞

协程的话,这个实现怎么样呢?

我看你在另一个贴子里好像有称赞过,不知道这个在实际使用的时候体验怎么样,好像没什么人关注这个……

async/await 是无栈协程, 他的 yield point 也是静态的, 这样就不符合我的需求了, 因为要用用户的按键来切换上下文, 用户什么时候按键显然是无法静态判断的.

参考

http://warmcat.org/chai/blog/?p=5558

2 个赞

这其实也是有一定风险的。

比如正在执行的代码占用太多CPU而导致loop没有机会进行调度, 整个协程调度被卡住了, 在Elisp就是input-pending-p有可能没有机会得到执行。

或者解析太多对象, 协程倒是可以保存数据到栈, 但是吐出的数据太多, 导致GC在主线程就卡住不动, 下一个协程没法运行。

在这种GUI或者CPU密集型的应用中, 效果最好的是多线程(操作系统调度)和多进程模型, 协程这种抢占式的设计, 最大的bug在于每个协程都很忙, 不让出空闲时间, 其他协程就没法运行, 主动每个周期让出固定时间去协调协程就会遇到让出的时间多少的问题? 让少了, 有些协程不够用, 让多了, 其实白白浪费了CPU.

5 个赞

毕竟最优解在可以预见的未来还无法实现, 协程应该是妥协条件下比较好的一个设计

image

2 个赞

with-no-input 是在 maybe_quit 实现的,不需要调用 input-pending-p

2 个赞

我觉得这样改难度也挺大的, 和支持多线程相比可能没有差那么多…

协程能随时打断应该有个前提条件, 就是函数内基本没有 side effects, 如果有的话一般要小心设计或者做同步.

emacs 当初设计的时候根本没考虑这些, c 代码里面各种 global 变量太多了, 个人感觉比 kernel 里面密度都高不少, 很多函数都会操作这些 global, 相互作用很难理清楚. 一个协程被用户操作中断之后, 哪怕是动了下光标, 可能已经改动了几个 global, 协程恢复运行之后就会有状态不一致的问题. 我感觉支持多线程的主要困难其实也在这里…

================

想了想 c 里面的其实还好, 毕竟有迹可循, lisp vm 里面还有一大堆, 这些感觉更头疼…

3 个赞

今天试了下emacs-ng的webrender 在mac上,结果卡的要死。 哎!希望有大佬能用rust写一个elisp的解释器,用opengl等技术解决渲染的问题。

emacs的生态逻辑是我用过最舒服,最趁手的,感觉emacs的发展被emacs本身阻滞了,有生之年要是能看到大佬重构emacs就好了。

1 个赞

你说的这些 C 端的 global 其实已经被移入 struct thread_state,Lisp 那边可能更烦人。

有没有读:emacs 转 rust 的又一波冲击 - #2,来自 oldosfan

Emacs 这种其实 Cairo 绘制就最高性能, OpenGL 绘制文字不会比这些矢量库更快。

图形应用的性能(用户感知到的界面刷新性能, 而不是计算性能)还是要靠多线程。

协程再怎么弄都会引入两个 event loop 非常麻烦。

1 个赞

有不少gpu加速的terminal,比如kitty、Alacritty、wezterm、warp等,感觉opengl绘制文字速度很快啊。

我们很多人混淆了执行性能和渲染性能。

执行性能的场景是用户不需要干预的情况下,就像rust比python快,gpu终端刷屏比2d矢量图快。

但是图形方面,绘制一个屏幕,特别是像emacs这样的文字绘制,gpu和cairo性能差距不明显,更多要关注多线程的技术不要卡住用户。

4 个赞

这些东西都很混,实际上的速度比不过 xterm,内存占用就更别说了。

而且字符一样是拿 FreeType 在 CPU 上绘制,最后上传至 GPU 进行绘制,Cairo 通过 X 服务器也是走的同一个流程。

2 个赞

emacs 的体验我感觉和语言上没有太大的关系,主要是架构设计上导致的一些弊端。希望是rust是因为想让emacs再能用个100年