R 能在 Apple 硅芯片上运行吗?



今年早些时候在 WWDC 2020 上,Apple 宣布从 Intel 处理器向基于 ARM 的处理器过渡。这篇博文基于在运行 A12Z(“Apple 硅芯片”处理器之一)的开发机器上进行的实验,探讨了 R 何时可以在该平台上运行的前景。

新平台将包括 Rosetta 2,这是一个动态翻译框架,它使用即时、动态二进制代码翻译来运行为 64 位 Intel Mac 构建的二进制文件。好消息是 R 似乎可以很好地与动态翻译配合使用,因此即使 R 用户使用当前版本,也不必担心。然而,有趣的问题是 R 是否也能以原生方式运行。预计原生执行速度会更快,而且无论如何,最终可能必须进行转换。

执行摘要是:虽然目前没有针对该平台发布 Fortran 编译器,但 GNU Fortran 的开发版本似乎已经可以正常工作。NaN 有效负载传播导致在使用数字 NA 计算时出现意外结果,这会产生一些令人惊讶的结果,但可以通过更改浮点单元的模式来克服这些问题,这已经在 R-devel 中完成。本文将详细介绍更多内容。

R 需要 Fortran 90 编译器

R 至少需要一个 Fortran 90 编译器才能构建。R 和基础包以及推荐包中的大多数 Fortran 代码仍然是 Fortran 77,因此它可以通过 f2c 翻译成 C 并由 C 编译器编译。但是,一些代码已经使用了 Fortran 90 特性,而回移植需要付出不小的努力。

此外,R 附带了一个经过略微修改的参考 LAPACK 和 BLAS 版本,它们需要 Fortran 90 编译器才能构建。即使 Apple 在 macOS 的 Accelerate 框架中提供了优化的 BLAS 和 LAPACK 版本,但最好在新平台上也能使用参考 LAPACK/BLAS。

GCC 的 GFortran 支持 64 位 ARM:5 月份的一篇早期博文是关于在 QEMU 仿真器内运行 64 位 ARM (Aarch64) 上的 Linux 上构建和测试 R 的。然而,Apple 硅芯片平台使用不同的应用程序二进制接口 (ABI),而 GFortran 还不支持。

目前,似乎没有其他 Fortran 90 编译器,无论是免费的还是商业的。具体来说,LLVM 的 Fortran 编译器(现在又称为 Flang)尚未完成。

构建 GCC/GFortran 的开发版本

虽然 GFortran 还不支持 Apple 芯片(既不支持任何版本,也不支持 GCC 主干),但 Iain Sandoe 有一个包含 GFortran 的 GCC 私有开发分支,我们对此进行了试验。

像往常一样从源代码构建 GCC 首先需要为该平台构建 GMP、MPFR 和 MPC。GMP(版本 6.2.0)需要从主干回传一个补丁(Apple 芯片的配置脚本、汇编器宏)。重新生成配置脚本和 make 文件还需要构建 libtool。即使是本机构建,也必须使用 --build=aarch64-apple-darwin20.0.0 显式运行 GMP 的配置脚本。使用 --with-sysroot 运行 GCC 的配置脚本,指定通过 xcode-select -p 安装的 MacOSX.sdk 目录。

测试 R

R 需要许多依赖项,可以按照 R 安装和管理 使用 Apple 提供的 Apple/LLVM 工具链(不需要 Fortran 编译器)进行本机构建。我们还编译了 Subversion 的本机版本,尽管原则上可以从 tarball 构建 R。

R 和推荐的包使用 Apple/LLVM clang 编译 C(使用 CFLAGS=-Wno-error=implicit-function-declaration)和 Objective C,并使用 GFortran 的开发版本编译 Fortran 代码。

R 和推荐包的许多测试因特定于平台的原因而失败,但事实证明所有原因都是相同的:NaN 有效负载的意外传播,例如 NA * 1NaN

NA/NaN 有效负载传播

R 的浮点数 NA 使用具有特殊有效负载值的 NaN 表示。源自不涉及 NA 的计算的 NaN 具有不同的(例如零)有效负载,因此可以与 NA 区分。NaN 经常在没有显式检查的情况下传递给 R 内部计算,并且在包代码和外部数值代码中也会发生这种情况,它们不知道 R 的 NA 概念或表示。

IEEE 754 浮点运算标准没有规定如何通过计算传播 NaN 有效负载。涉及 NA 和/或其他 NaN 的计算结果取决于 CPU/浮点单元、编译器优化(编译器可能会重新排序计算)和算法(例如,在假设输入值是有限的情况下,忽略计算结果不需要的输入值很诱人,但实际上并没有检查它们是否有限)。

可以在 R 的在线帮助(?NA?NaN)中找到更多信息,包括不应依赖 NA 和 NaN 之间差异的免责声明(引用最近 R-devel 的措辞)

涉及“NaN”的计算将返回“NaN”或可能是“NA”:这两个中的哪一个不能保证,并且可能取决于 R 平台(因为编译器可能会重新排序计算)。

使用“NA”进行数值计算通常会导致“NA”:一个可能的例外是还涉及“NaN”的情况,在这种情况下,可能会产生任一结果(这可能取决于 R 平台)。但是,这并不能得到保证,未来的 CPU 和/或编译器可能会表现出不同的行为。

Intel FPU 在 R NA 方面表现得相对较好:即使涉及 NaN,二进制运算也会产生 NA(基于实验)。但是,目前在 Intel 机器上运行的 R 通常被构建为使用 SSE 指令进行计算,而这些指令对 R NA 的处理效果不佳:只有当另一个参数不是 NaN 或 NA 为第一个参数时,二进制运算才会产生 NA,因此NaN + NANaN,但NA + NaNNA。由于带有 SSE 的 64 位 Intel 很可能一直是 R 最近流行的设置,因此测试已经更新为接受此行为。

但是,事实证明,在 A12Z 上,R 的 NA 在任何二进制运算后都会变成一个正常的 NaN(有效负载丢失),因此即使NA * 1也是NaN。这在上面引用的 R 文档中已得到警告,但仍有许多针对 R 和推荐包的测试捕获了(记录为不可靠的)行为,期望此类运算将返回NA。我们尚未调查有多少其他 CRAN/BIOC 包这样做。

ARM 架构浮点单元(VFP、NEON)支持 RunFast 模式,其中包括冲零和默认 NaN。后者意味着 NaN 操作数的有效负载不会传播,所有结果 NaN 都具有默认有效负载,因此在 R 中,即使NA * 1也是NaN。幸运的是,RunFast 模式可以禁用,并且在禁用时,NaN 有效负载传播比 Intel SSE 对 R NA 更友好(NaN + NANA)。因此,我们已更新 R,以便在启动时在 ARM 上禁用 RunFast 模式,从而解决了观察到的所有问题。

我们之前没有遇到此问题,因为 RunFast 在其他平台上默认已禁用,包括 Raspberry Pi(在带有 32 位 ARM、BCM2835 的旧型号 2 上测试)和模拟 Aarch64(64 位 ARM、Cortex-A72)的 QEMU 上。

摘要

事实证明,R 有望在 Apple 芯片上运行。希望很快就能推出适用于 Apple 芯片的可用 Fortran 90 编译器,因为 GFortran 的开发版本似乎已经可以工作(R 的check-all通过,包括参考 LAPACK/BLAS),并且不仅 R,而且该平台上的任何科学计算都迫切需要此类编译器。

任何希望可靠地保留 NA 的包本机代码(输入中至少有一个 NA 值的计算在输出中提供 NA)都必须包含显式检查,无论是针对在包本机代码中实现的计算还是针对外部库中的计算。这是唯一可移植、可靠的方法,并且长期以来一直是唯一的方法。另一方面,选择不保证这种传播的包不应在测试中捕获开发人员平台上的偶然传播。在 ARM 上,因此在 Apple 芯片上,R 现在通过禁用 RunFast 模式来掩盖其中一些问题,但可能会出现另一个新平台,在该平台上这是不可能的,更重要的是,NA 也可能由于外部库中的编译器优化或算法而“丢失”。

参考

  1. Apple 芯片 ABI。Apple 芯片上的应用程序二进制接口 (ABI) 的具体信息。

  2. GCC Bugzilla 增强/错误报告,其中包含有关 GCC/GFortran 对 Apple 芯片支持的开发更新。

  3. Apple 芯片的实验性 GCC Iain Sandoe 提供的 GCC 开发分支,支持 Apple 芯片。

  4. Apple 芯片的 GMP 支持。Torbjorn Granlund 提供的 Apple 芯片的 GMP 支持。

  5. 浮点异常跟踪和 NAN 传播 Agner Fog 撰写的有关 NaN 有效负载传播的文本。

  6. ARM RunFast 模式 (https://developer.arm.com/documentation/ddi0274/h/programmer-s-model/compliance-with-the-ieee-754-standard/ieee-754-standard-implementation-choices) RunFast 模式与 IEEE 754 的差异说明。