R 是否可在 64 位 ARM Windows 上运行?



今年早些时候在 WWDC 2023 上,Apple 宣布已完成从英特尔到 64 位 ARM 处理器(Apple 硅)的过渡:将不再提供搭载英特尔处理器的全新机器。这是在 WWDC 2020 上宣布过渡三年后。对该平台的 R 支持工作始于同一年,并成为下一版 R(R 4.1)的一部分。请参阅此 博文,了解在该平台上对 R 进行的初始实验的详细信息。

Windows 上的情况截然不同。2017 年发布的 Windows 10 已支持 64 位 ARM 处理器,但它没有用于运行 64 位英特尔二进制文件(仅 32 位)的模拟器,因此许多应用程序无法运行。当时的硬件可用性有限:只有平板电脑(或类似平板电脑)选项,但没有用于使用该系统的快速笔记本电脑或工作站。如今,Windows 11 可用,并支持对 64 位英特尔应用程序进行模拟。Apple M2 和 M1 机器可用于(通过虚拟化)运行 Windows,具有讽刺意味的是,甚至 Microsoft 也将其推荐为一种选择。Mac 用户可能更愿意使用 macOS 版本的 R,但可用于完全原生运行 Windows 的 ARM 机器硬件可用性也在不断提高。那么,R 是否可以在 64 位 ARM 上的 Windows 上运行?

执行摘要如下:虽然目前没有针对该平台发布稳定的 Fortran 编译器,但 LLVM/flang 已经足够好,至少可以构建基础 R 和推荐的包。在 Windows 上构建 R 只需要进行合理的更改即可使用 LLVM。初步实验表明,将 Rtools43 扩展为支持 LLVM/aarch64 是可能的。本文将详细介绍更多内容。

用于测试的硬件

这些实验是在一台搭载 Apple M1 Pro 处理器的 MacBook Pro 机器上进行的,该机器运行的是带有 Windows 11 的虚拟机 (QEMU/UTM)。运行检查和编译的性能令人满意。

模拟

为 64 位英特尔机器构建的 R 4.3.1 在 64 位 ARM 上的 Windows 11 上运行,但由于数值差异,它未通过安装测试(R 管理手册中的 测试安装)。需要进行额外的测试和分析,才能得出结论,即这些差异(很可能是由模拟器引起的)是否仍然可以接受。

64 位 ARM 上的 Windows 还允许混合模拟,其中在单个地址空间中,部分代码为 64 位英特尔 (x86_64) 构建,但另一部分可以构建为在 64 位 ARM 上原生运行,但使用名为 arm64ec 的 Microsoft 专有 ABI。Arm64ec 基于 x86_64 调用约定,Windows 本身也使用它。然后,可以在单个 arm64x 二进制文件中将“真正的”64 位 ARM 二进制文件(称为 arm64 或 aarch64,本文中使用后者)与 arm64ec 二进制文件结合起来。

此处报告的实验仅针对 aarch64,因此在单个地址空间中运行的所有代码(R、R 包和外部库)都必须为 aarch64 构建。预计 Aarch64 代码在该平台上具有最佳性能和最低功耗。

R 需要 C 和 Fortran 编译器

R 需要一个 C 和 Fortran 编译器才能构建。Windows 版 R 传统上使用 GNU 编译器集合 (GCC) 构建:gcc 用于 C 代码,gfortran 用于 Fortran 代码。虽然 GCC 在 Linux 上无缝支持 aarch64,并且至少有一个 GCC 开发分支支持 macOS 上的 aarch64,但 Windows 上没有现成的此类 GCC 支持。可以在 GCC bugzilla 上关注最近为添加此支持所做的努力。

另一方面,LLVM 多年来一直支持 Windows/aarch64(通过 MinGW-W64),至少从 2018 年开始,请参阅此 线程 了解更多详情。然而,虽然 R 可以使用 LLVM 中的 clang(C 编译器),但 LLVM 中没有可用的 Fortran 90 编译器选项(对于任何平台),也没有任何其他适用于 Windows/aarch64 的免费 Fortran 90 编译器。

就在今年年初,LLVM 中的 flang 编译器(有时称为 flang-new,因此不是经典的 flang 编译器)变得可用,它可以构建包括基本和推荐包在内的 Linux 版 R。Unix 平台上新 flang 的测试和额外支持由 Brian Ripley 完成,他还测试了 CRAN 包,并与它们的作者合作对支持 Unix 上的 flang 进行了必要的调整。

flang 的新进展最终使得实际尝试构建 Windows/aarch64 版 R 成为可能。

必需的更改

R 的大部分内容正在由不同的编译器(包括 LLVM clang 和最近的新 flang)和不同的平台(包括 aarch64)进行测试,因此大部分代码不需要任何更改。然而,R 中特定于 Windows 的代码最近只与 GCC、binutils 和英特尔处理器一起使用,并且有时会无条件地假定这些内容。必需的更改如下。

几段英特尔汇编代码被 aarch64 汇编、C 代码替换,或被移除(进入调试器、重置 FPU 状态、交换字节)。

C 代码必须经过现代化改造,以使 LLVM clang 16 满意。GraphApp 符号和系统标头之间的命名冲突已得到解决。一些旧的已合并代码中的 K&R 表示法必须被替换。函数指针类型已扩展为也列出参数类型。没有原型的函数必须获得一个,例如,当函数不接受任何参数时,显式添加 void 作为参数。

必须更新对 dllimportdllexport 属性的使用,以防止在重新声明中添加属性时出现的情况,这被视为错误:dllimport 根本不需要,因此可以省略,而 dllexport 不在 R 中使用,因为 DLL 中的导出是通过定义文件处理的。

必须避免对整数调用 isnan,因为它会导致崩溃,但无论如何都不应该这样做,并且通常会导致警告。Clang 将自身标识为相当旧的 GCC,因此在一种情况下,旧的未使用代码被无意中使用,因此为 clang 添加了适当的条件。

指定 R 版本的文件版本元数据必须更新,因为资源编译器 llvm-rc 要求 FILEVERSION 的每个元素都适合 16 位。R 的当前 SVN 版本不再适合 16 位。

必须在检测当前平台和正在使用的编译器的函数中添加对 Windows/aarch64 的支持,例如出现在 sessionInfo() 中。必须为该平台提供所选编程语言函数和底层运行时符号名称之间的映射(sotools,这些在包检查中使用)。

Windows make 文件获得了对 LLVM 编译器和工具的初始支持(USE_LLVM)。

现在 WIN 变量可以为空,表示未知/未指定架构(64/32 表示 64 位和 32 位 Intel),默认情况下将根据源代码编译包,并且不会使用子架构。使用此设置,例如 Rterm.exe 将位于 bin 中,而不是 bin/x64。这类似于 R 在 Unix 上的默认工作方式。

这些更改是实验性的,如有需要,可能会对其进行修改或删除。

Rtools

从 Rtools42 开始,Rtools 是 Msys2 构建工具、MXE 构建的编译器工具链+库和几个独立工具的集合。Msys2 构建工具和 x86_64 的独立工具可以通过模拟在 Windows 11/aarch64 上使用。编译器工具链和库必须不同。

MXE 是一个交叉编译环境。编译器工具链和库都构建在 Linux/x86_64 上。因此,可以将现有基础设施(包括硬件)用于构建和实验。

尽管 MXE 还不支持 LLVM(请参阅 错误报告,其中可以跟踪此问题),但据报道 MXE 已与 Martin Storsjo 在 libvips 项目 范围内略微定制的 llvm-mingw 工具链一起使用。然而,Llvm-mingw 不支持 flang。

因此,Rtools43(特别是其 MXE 分支)已扩展为支持 LLVM 16.0.5,包括 flang 作为 MXE 插件,重新使用 llvm-mingw 脚本及其 libvips 修改。

与 llvm-mingw 不同,只支持一个 LLVM 目标,以匹配 Rtools 在 R 项目中的使用方式并适应 MXE 目标目录布局。因此,使用相同的代码库,可以为 x86_64 构建一个捆绑包(编译器工具链和库),为 aarch64 构建一个完全独立的捆绑包。

在构建工具链时,首先在 Linux/x86_64 上构建 LLVM 交叉编译器。它们在 Linux/x86_64 上运行,并生成在 Windows/aarch64 上运行的代码。使用这些交叉编译器,构建在 Windows/aarch64 上运行并生成在 Windows/aarch64 上运行的代码的 LLVM 本机编译器。构建本机 flang 编译器需要一个额外的自举步骤来解决交叉编译 MLIR 中的问题(从 Julia 如何交叉编译 MLIR 中获得灵感很有帮助)。

Rtools43 中几乎所有但并非全部针对 x86_64 的库都已针对 aarch64 编译,但编译的库比构建基本 R 和推荐包所需的要多得多。实验性构建与针对英特尔平台的构建类似,以三个 tarball 的形式提供llvm16_ucrt3_base_aarch64_*.tar.zst 是构建基本 R 和推荐包所需的基本工具链和库,llvm16_ucrt3_full_aarch64_*.tar.zst 是包含更多库的超集,llvm16_ucrt3_cross_aarch64_*.tar.zst 用于交叉编译。

然后使用交叉编译器为 R 构建 Tcl/Tk 捆绑包,该捆绑包以 Tcl-aarch64-*-*.zip 的形式提供。构建 Tcl/Tk 只需要少量更改即可支持 LLVM 16,因为否则它已经支持 aarch64。

但是,当软件尚未针对 Windows/aarch64 更新时,构建 Rtools43 的库需要进行大量更改。它们与 R 本身所需的更改性质类似,如下所示。

一些手写汇编代码优化例程,当不适用于 aarch64 或依赖于 GNU 汇编器时,必须禁用。一些自动配置文件必须使用新的 autoconf 重新生成以支持 aarch64,而这反过来有时需要对项目进行进一步修复。一些 MXE 构建脚本必须调整为不假设 Windows 上的英特尔 CPU。必须处理(更严格的)C 编译器报告的一些编译器警告和错误,主要是重新函数原型和 K&R 表示法。在某些情况下,升级库有助于获取支持 aarch64 的代码,但随后需要额外的修复才能使其正常工作。只要有可能且可用,就会从Msys2中重新使用对这些库的修复。

然后使用 Rtools 在 Windows 11/aarch64 上本机构建 R 和推荐包,类似于在英特尔机器上,使用工具链+库 tarball 和单独的 Msys2 安装。

用于在用于 Rtools43 的 docker 容器中自动化构建 Rtools 和 Tcl/Tk 捆绑包的脚本已扩展为支持 Windows/aarch64。脚本和对 Rtools 的所有实验性更改都可以在通常的位置找到(工具链+库Tcl/Tk 捆绑包)。

最初的实验是使用 Msys2 提供的编译器工具链和库进行的。只有在明确所需的更改量合理后,才为 aarch64 构建了 Rtools,并清理了必要的更改,使用 Rtools 进行了测试,并提交给了 R 的开发版本 R-devel。

如何进行实验

可以使用由以下内容组成的 MkRules.local 构建 R-devel

USE_LLVM = 1
WIN =

使用 Rtools 完整(或基本)工具链+库包和 Tcl/Tk 包,两者均可 在此处 获得。通过这种方式构建的 R 将自动仅从源安装包。

摘要

在撰写本文时,通过这种方式构建的 R(包括推荐的包)通过了其测试。

仅凭这一点不足以对新的 flang 进行充分的测试,但 Brian Ripley 已经在 Linux 上对 CRAN 包进行测试,以使其与 flang 配合使用,结果很有希望。在 Windows 上进行的未来测试(尤其是在升级到较新的 flang 编译器后,因为此编译器存在已知问题)有望不会发现太多新问题。

已经为 aarch64 构建了几乎完整的 Rtools,这应该允许测试大多数 CRAN 包(但请参阅下一部分)。

对于基本 R 和作为 Rtools 一部分构建的软件所需的代码更改并不繁琐,其中大部分是清理和现代化,无论如何,该软件都必须在某个时间点进行这些更改。

我认为 R 能够在 Windows 上使用 LLVM 编译器是有价值的,而不仅仅是为了在 aarch64 上支持 Windows。未来可能会出现其他限制或用途,包括新平台、与新语言的互操作性或新工具的使用。支持多个编译器显然有助于保持代码的可移植性。

R 包也是如此。无论 aarch64 上的 Windows 是否会成为一个重要的平台,以及是否会很快实现,可移植且支持多个编译器(尤其是 LLVM),即使在 Windows 上,也是一件好事。

关于测试包

使用新的工具链或在工具链发生重大更改后测试包时,一个主要的痛苦来源是包下载预编译代码。此类代码不是,也不可能兼容:当然,英特尔的静态库不能链接到 aarch64 的可执行文件或动态库中。

当下载由具有大量反向依赖项的包完成时,这使得对新工具链或已显著更新的工具链进行任何测试基本上是不可能的:大多数 R 包无法构建和测试。这也是 CRAN 存储库策略 通常不允许此做法的原因之一。请参阅 操作方法:在 Windows 上构建 R-devel 和包,了解此做法为何真正糟糕的其它原因。与 Rtools 本身进行比较,它没有此问题:MXE 包不会下载任何预编译代码,因此可以轻松切换工具链并从头开始重新构建。

在 Windows 上从 MSVCRT 过渡到 UCRT 期间,存在类似的问题,当时几乎所有 CRAN 包的库都已创建并添加到 Rtools42 中。为了测试它们,已修补了 100 多个 CRAN 和一些 Bioconductor 包以使用 Rtools 中的库,并且在测试时自动应用了这些修补程序。不幸的是,许多包未合并此类修补程序,而是最终再次下载预编译代码(然后用于 UCRT),重复了我们现在再次遇到的先前问题。

如果仍然下载预编译代码的 CRAN 包可以修复为不这样做,那将是极大的帮助。在某些情况下,他们可能能够从 UCRT 过渡期间创建的较旧修补程序中找到灵感,这些修补程序可 在此处 获得。

有关在 Windows 上处理外部库的附加信息,请参阅 操作方法:在 Windows 上构建 R-devel 和包,包括有关将库添加到 Rtools(将包添加到 MXE)的提示。