让官方编译的 Windows 版 Emacs 29.2 的 native-comp 特性生效

昨天我一直关注的类型声明分支总算是合并了:Merge branch ‘lisp-func-type-decls’ into ‘master’,理论上添加函数的类型声明后 native-comp 能够更有效地优化代码,不过具体的优化代码似乎还没有写。看到这个分支合并后我在 Windows 上用 MSYS2 拉下来编译了一下,没啥感觉(汗)。

这个帖子是介绍我在折腾 native-comp 的时候找到的不用在 MSYS2 的 Mingw64 SHELL 中启动 Emacs 也能用上 native-comp 的方法。使用的方法来自: MS-Windows版 Emacs 29.1への移行作業 | Misohena Blog

Emacs 官方提供的官方 Windows 编译版本

在 Emacs 的 FTP 里面提供了编译好的 Windows 版本可供下载,下载后运行 bin 目录下的 runemacs.exe 即可启动 Emacs。以 Emacs 29.2 为例,在 bin 目录下已经提供了许多 DLL 文件,但其中没有 libgccjit 相关的 DLL。

虽然我们在运行 (native-comp-available-p) 时得到的结果是 nil ,但是查看 system-configuration-options 时可以注意到它在编译时启用了 native-comp 特性:

system-configuration-options is a variable defined in ‘C source code’.

Its value is
"--with-modules --without-dbus --with-native-compilation=aot --without-compress-install --with-tree-sitter --with-sqlite3 CFLAGS=-O2"

String containing the configuration options Emacs was built with.

[back]

使 (native-comp-available-p) 返回 t

根据 Windows 的 DLL 查找规则,我们将 gccjit 相关的 DLL 放在 Emacs 的 bin 目录下即可。我们可以在 MSYS2 的 Mingw64 SHELL 中通过以下命令安装 libgccjit:

pacman -S mingw-w64-x86_64-libgccjit

安装完成后,MSYS2 的目录中的 mingw64/bin 中会出现一个名叫 libgccjit-0.dll 的文件,但光是把它拖到 emacs 的 bin 目录下还不够,它依赖这些 DLL:

其中除系统 DLL 和 libisl-23.dll, libmpc-3.dlllibmpfr-6.dll 外,Windows 版 Emacs 均已提供,因此我们只需要将 mingw64/bin 下的以下 DLL 移动至 Emacs 的 bin 目录下即可:

  • libgccjit-0.dll
  • libisl-23.dll
  • libmpc-3.dll
  • libmpfr-6.dll

完成以上操作并重启 Emacs 后,调用 (native-comp-available-p) 应该会得到 t

添加 as.exeld.exe

虽然此时 native-comp 特性已经可用,但当你切换到 *Async-native-compile-log* buffer 时,你会注意到编译因为找不到 as 而失败。(我在出现这个错误的时候忘记截图了,这是来自一篇文章的输出结果: Windows版Emacsを28.1に上げたのでNative Compilationフィーバーに便乗する

Compiling c:/emacs/share/emacs/28.1/lisp/emacs-lisp/cconv.el...
x86_64-w64-mingw32-gcc-11.3.0: fatal error: cannot execute 'as': CreateProcess: No such file or directory
compilation terminated.

参考在帖子开头给出的教程,我们可以在 Emacs 的 lib 目录下创建 gcc 目录,然后从 mingw64/bin 中复制一份 as.exeld.exe 过来,记得带上它们依赖的 DLL:

  • as.exe
  • ld.exe
  • libzstd.dll
  • zlib1.dll

添加文件后,在自己的配置文件中添加以下配置来指定这些文件的路径:

 (setq native-comp-driver-options (list "-B" (expand-file-name (file-name-concat invocation-directory "../lib/gcc"))))

此时再次重启 Emacs, *Async-native-compile-log* 中的错误会变成一系列的缺少库文件:(我也忘了截图了,只能偷别人的):

Compiling c:/emacs/share/emacs/28.1/lisp/language/japan-util.el...
ld: dllcrt2.o が見つかりません: No such file or directory
ld: crtbegin.o が見つかりません: No such file or directory
ld: -lmingw32 が見つかりません
ld: -lgcc_s が見つかりません
ld: -lgcc が見つかりません
ld: -lmoldname が見つかりません
ld: -lmingwex が見つかりません
ld: -lmsvcrt が見つかりません
ld: -lmingw32 が見つかりません
ld: -lgcc_s が見つかりません
ld: -lgcc が見つかりません
ld: -lmoldname が見つかりません
ld: -lmingwex が見つかりません
ld: -lmsvcrt が見つかりません
ld: crtend.o が見つかりません: No such file or directory
c:\emacs\bin\libgccjit-0.dll: error: error invoking gcc driver
c:/emacs/share/emacs/28.1/lisp/language/japan-util.el: Error: Internal native compiler error failed to compile

根据帖子开头的教程,需要从以下位置复制文件到 Emacs 目录中的 lib/gcc 目录下:

[mingw64/lib]
crtbegin.o
crtend.o
dllcrt2.o
libadvapi32.a
libgcc_s.a
libkernel32.a
libmingw32.a
libmingwex.a
libmoldname.a
libmsvcrt.a
libpthread.a
libshell32.a
libuser32.a
[mingw64/lib/gcc/x86_64-w64-mingw32/13.1.0/]
libgcc.a

再次重启 Emacs ,应该就能正常完成编译了。

Summary

以下是我现在使用的关于 native-comp 的配置:

;;@@NATIVE-COMP
;; https://misohena.jp/blog/2023-07-31-setup-emacs-29-1-for-windows.html
;;te 在 bin/ 下添加必要库
;; 注意 bin/ 中 libgccjit-0.dll 存在以下依赖项:
;; 可使用 https://github.com/lucasg/Dependencies 获取 DLL 的依赖项
;; system32/{advapi32.dll, kernel32.dll, MSVCRT.dll}
;; libgcc_s_seh-1.dll, libgmp-10.dll
;; libisl-23.dll, libmpc-3.dll, libmpfr-6.dll ; 注意这三个
;; libwinpthread-1.dll, zlib1.dll, libzstd.dll
;; 随后,在 /lib/gcc/ 下添加以下内容:(一共 18 个)
;; mingw64/bin/ 中的 as.exe 和 ld.exe,它们依赖同目录下的 libzstd.dll 和 zlib1.dll
;; mingw64/lib 下的以下文件:
;; crtbegin.o, crtend.o, dllcrt2.o
;; lib{ advapi32.a, gcc_s.a, kernel32.a, mingw32.a
;;      mingwex.a, moldname.a, msvcrt.a, pthread.a
;;      shell32.a, user32.a}
;; mingw64/lib/gcc 中的 libgcc.a

;; 忽略 native-comp 过程中的 warning
(setopt native-comp-async-report-warnings-errors nil)
;; 添加编译的工具和库位置
(when (and (fboundp #'native-comp-available-p)
	   (native-comp-available-p)
	   (eq system-type 'windows-nt))
  (setopt native-comp-driver-options
	  (list "-B" (expand-file-name (file-name-concat
					invocation-directory
					"../lib/gcc")))))

我原本打算重现一遍我之前遇到的错误,但即使我注释掉了上面的代码,native-comp 似乎也能正常工作,很奇怪…

也许之后版本的 Windows 版 Emacs 会提供 native-comp 依赖项,也就不用我们手动添加了。

2 个赞

这又有msys、又有win32环境,是不是msys本身就可以一步到位的? 注释了仍然能工作,猜测:有没有可能那些依赖dll本就在path环境变量里,无需复制?

1 个赞

直接在 MSYS2 的 Mingw64 SHELL 里启动 Emacs 是能够正常使用 native-comp 特性的,这个帖子主要是解决不在这个环境启动 Emacs 的情况。

我在写的时候检查过环境变量,我没有添加 mingw64/bin 到环境变量里面。现在看了一下也没有。不知道 native-comp 是不是有什么设定保存,这个得研究一下。

正好官方编译的 Emacs 29 有三个,29.1, 29.2 和 29.3。我在 29.2 里尝试了添加这些文件到 emacs 的 bin 目录和 /lib/gcc 目录并修改 native-comp 的相关选项。那接下来我试试测试 29.1 时添加 mingw64/bin 的环境变量,在 29.3 试试仅第一次在 MSYS2 环境中启动 Emacs,并测试随后是否能够正常进行 native-comp 编译,然后输出文件到 eln-cache 中。

emacs 29.1

在 Emacs 29.1 中,我同样将上个帖子提到了的和 libgccjit 有关的 4 个 DLL 放到了 Emacs 的 bin 目录下,当我没有设定到 mingw64/bin 下的环境变量 PATH 时,通过 emacs -Q 打开 Emacs 会出现如下 Warning:

在我在环境变量 PATH 中加上 mingw64/bin 路径后,再次打开 emacs -Q ,可以得到如下结果:

再次取消掉环境变量 PATH 中的 mingw64/bin,打开 emacs -Q ,然后通过 pacakge-install 随便安装一个包,可以看到如下结果:

这应该能说明添加 mingw64/bin 目录到 PATH 中可使 native-comp 正常工作。

emacs 29.3

和上一步一样,添加 4 个 DLL 到 Emacs 29.3 的 bin 目录下,首先在 cmd 中使用 emacs -Q 启动 Emacs:

然后在 Mingw64 SHELL 中通过 emacs -Q 启动 Emacs,package-initialzie 之后随便装个包:

再次在 cmd 中使用 emacs -Q 启动 Emacs,然后在 pacakge-initialize 后随便装个包:

可见在非 Mingw64 SHELL 环境中,仅添加 libgccjit DLL 及相关项到 Emacs 的 bin 目录中并不能让 native-comp 正常工作。

emacs 29.2

同样,首先添加 libgccjit 相关库到 Emacs bin 目录下,此时若直接运行 emacs -Q 得到的结果应该和前面一致,就不展示了。

接下来添加 as.exeld.exe 以及一些静态库文件到 Emacs 的 /lib/gcc 目录下,然后在 emacs -Q 启动后执行以下代码:

(setopt native-comp-driver-options (list "-B" (expand-file-name (file-name-concat invocation-directory "../lib/gcc"))))

接下来, package-initialize 后随便安装一个包,得到以下输出:

关闭 Emacs,随后通过 emacs -Q 启动 Emacs,但不执行以上代码,再通过 package-install 随便安装一个包:

这是让我感觉最奇怪的地方,似乎 native-comp-driver-options 只需一次设定,之后 native-comp 就“记住”它了…

草,这就说明只需要添加 native-comp 依赖项到 /lib/gcc 目录下,native-comp 就能找到这些东西,也就不需要设定 native-comp-driver-options 了。

补充

native-comp-driver-options 的用处可以参考 libgccjit 的文档:Compilation contexts — libgccjit 15.0.0 (experimental ) documentation 的末尾,上面设置 -B 参数的作用是添加搜索路径。

看来只需要把 libgccjit 的依赖项放到 Emacs 的 /lib/gcc 目录下,它就会自动查找了,不需要设定什么 native-comp-dirver-options

那么,最后总结一下,要让从官网下载的带有 native-comp 支持的 Windows 版 Emacs 能真正用上 native-comp ,我们只需要:

  1. 将 mingw64/bin 下的 libgccjit-0.dll, libisl-23.dll, libmpc-3.dll 和 libmpfr-6.dll 放到 Emacs 的 bin 目录

  2. 在 Emacs 的 lib 目录下创建 gcc 目录,并

    1. 添加来自 mingw64/bin 的 as.exe 和 ld.exe,以及它们的依赖项 libzstd.dll 和 zlib1.dll
    2. 添加来自 mingw64/lib 下的 crtbegin.o, crtend.o, dllcrt2.o 和 libadvapi32.a, libgcc_s.a, libkernel32.a, libmingw32.a, libmingwex.a, libmoldname.a, libmsvcrt.a, libpthread.a, libshell32.a, libuser32.a
    3. 添加来自 mingw64/lib/gcc/x86_64-w64-mingw32/13.1.0 的 libgcc.a

添加以上文件后,重启 Emacs 即可。

另,libgccjit 可通过以下命令安装:

pacman -S mingw-w64-x86_64-libgccjit
4 个赞

这么多lib,scoop能安装不?

没用过 scoop,应该不行…

scoop 可以直接安装编译好的

scoop bucket add kiennq https://github.com/kiennq/scoop-misc
scoop install emacs-k
3 个赞

看了一下编译脚本,考虑了native-comp,那确实应该可用

有点复杂啊!能解决问题就行!官方安装包以后能自动处理这些就好了

只用看解决方案的那个帖子就行了,之前我还以为需要配置其他东西,实际上只需要把这些文件放到对的位置就行,不用在配置文件中添加什么。

是的,字数补丁。

理论上添加函数的类型声明后 native-comp 能够更有效地优化代码

没关注过他们的工作,但是如果想要利用类型信息做优化的话应该是得证明一些东西(比如 soundness?)

1 个赞

在这个分支刚刚合并的时候我就拉下来编译尝试了一下,但是没什么效果。

(草,既然有人看,那我在这个帖子里总结一下这几个月 emacs-devel 上关于在 Emacs 的 declare 里面添加函数参数和返回值的类型声明来尝试优化 native-comp 的代码生成的进展。)

关于添加类型声明的最早讨论应该是在 2024 年 2 月的这一封邮件: Declaring Lisp function types, native-comp 的作者 Andrea Corallo(同时也是现在的 Emacs 主要维护者之一: A new Emacs maintainer: Andrea Corallo.)提议在函数的 declare form 里面使用类型。邮件里面的主要反对声音是 (declare (type 很难通过 grep 搜索。比较有意思的反对观点是这会加速 Emacs Lisp 的静态语言化 Re: Declaring Lisp function types。RMS 也参与了 2 月份的讨论。3月的讨论主要是类型声明的语法。 5月主要讨论了分支合并和类型声明可能导致的 UB。目前 Emacs 30 的 NEW 里面是这样说明的:

+++
*** New 'ftype' function declaration.
The declaration '(ftype TYPE)' specifies the type of a function.
Example:

    (defun hello (x y)
      (declare (ftype (function (integer boolean) string)))
      ...)

specifies that the function takes two arguments, an integer and a
boolean, and returns a string.  If the compilation happens with
'compilation-safety' set to zero, this information can be used by the
native compiler to produce better code, but specifying an incorrect type
may lead to Emacs crashing. See the Info node "(elisp) Declare Form"
for further information.

NEWS 里面使用的是 ftype 而不是 type ,主要是因为 5 月份的这一讨论。Andrea 在这个回复里面提到了一种可能会因为错误的类型声明导致 Emacs 崩溃的情况。目前的话, scratch/comp-branch-optim 分支似乎在尝试利用这些类型信息。

另外,Andrea 也尝试过在 Emacs 的 C 代码里面添加类型声明: Declaring primitive function types,具体的分支是 scratch/func-type-decls,但这个尝试似乎失败了。

soundness 指即使错误的类型声明也会不让 Emacs 崩溃吗。

2 个赞

(类型系统的)Soundness 本身有一些定义,比如 Preservation 和 Progress,直觉上的理解类似于 “类型系统接受的程序会不会在运行时出现类型错误”.

具体来说,Elisp 本身有一套求值规则 x ~> v,假设有一个 foo

(defun foo (x : int)
  (+ x 1))

(foo t)

如果类型系统不满足 Preservation,那可能出现一个难绷的情况,就是虽然声明了 t : int,类型系统(不知道出了什么毛病)也接受了,但是求值以后,t ~> v, v : string. 这样 (foo t) 会出现运行时的类型错误。

如果编译器使用 foo 声明的类型信息进行优化,那么很可能会导致 emacs 程序本身出现内存错误直接崩溃。TS,Python 的类型系统都要面临类似的问题,不过它们一开始似乎就放弃了通过类型声明进行优化,自然就不用管这个问题了。

其实,JIT 编译器的一个重要任务就是做类型推理,得到类型信息并优化。甚至,JIT 可以 “假设” foo 的参数就是 int 类型的,然后在发现这个假设错误的时候进行 deoptimization,V8、JVM 都有类似的机制。Emacs 的 native-comp 好像就没有这个机制,我觉得想办法加上类似的机制也很重要。

3 个赞

那么是不是在更新完成插件后再去mingw64下编译一次就好,平常不用把msys2的bin塞到环境变量中 native-comp 编译完成后就不需要编译环境了吧(它已经编译为机器码了,除非需要重新编译的情况)

当然可以,但是 msys2 本身占 10GB 。然后 native comp 只是编译自带的包冷启动长达 3 分钟。

1 个赞