GFortran 与 LAPACK II 的问题



这是我之前帖子 5 月份 的更新。

自那时起,许多事情发生了变化:GFortran 开始采用一种修复程序,该程序默认情况下会阻止优化,而优化会破坏在没有隐藏长度参数的情况下从 C 调用 BLAS/LAPACK 函数的代码。R 已更新为在内部添加这些隐藏的长度参数(以及在检测到 LTO 类型不匹配的其他情况下)。R 已导出宏以在程序包中使用,以便它们在调用 BLAS/LAPACK 时遵循此做法,而 CRAN 一直在与受影响程序包的维护者合作。另一方面,Linux 发行版中 BLAS/LAPACK 实现的二进制文件开始出现问题。Fedora 30 中的 OpenBLAS 是使用仍执行激进优化的 GFortran 版本编译的。因此,尚未修复为提供隐藏参数的 R 程序包在某些情况下可能会崩溃,并且确实会崩溃。

GFortran 中的变化

GFortran 9.2 已发布,并带有新选项 -ftail-call-workaround,该选项会禁用在具有隐式原型化过程的字符参数的过程中的尾调用优化。此选项默认启用,因此 GFortran 9.2 对于不将隐藏长度传递给 BLAS/LAPACK 字符参数(长度为 1)的代码来说再次安全。还可以使用 -ftail-call-workaround=2 在所有具有字符参数的过程中的禁用尾调用优化。因此,此选项比 -fno-optimize-sibling-calls 的侵入性更小,允许在更多情况下进行尾调用优化,但另一方面,它被声明可能会在 Fortran 的未来版本中被取消。还声明了该选项的默认值可能会更改。此选项也已添加到 GFortran 7 和 GFortran 8 中,但尚未发布带有此更改的版本。感谢 Jakub Jelinek 实现此新选项。

R 尚未使用新选项,而是仍然使用更保守的选项 -fno-optimize-sibling-calls。可以根据配置测试切换到新选项,该测试将检查该选项是否可用,如果不可用,则恢复为 -fno-optimize-sibling-calls

此(可能是临时)选项对 R 的主要好处是它默认启用:尚未使用 -fno-optimize-sibling-calls 或另一个阻止危险优化的选项明确构建的 LAPACK 和 BLAS 实现将在一段时间内再次安全,即使从尚未修复的 R 程序包中使用也是如此。同样适用于 R 之外,甚至 LAPACKE 和 CBLAS 也需要修复。

R 中的变化

R 中包含的 BLAS 和 LAPACK 头文件已得到扩展,以便 BLAS 和 LAPACK 函数的 C 声明也包括隐藏长度参数。已修复 R 自身 C 代码中对 BLAS 和 LAPACK 的所有调用,以传递此参数(对于计算函数,它始终为 1)。此项工作已由 Brian Ripley 完成。

隐藏长度参数的实际类型以及是否使用它可以通过 R 中的宏(FCONEFC_LEN_T,在函数声明中内部为 FCLEN)进行配置,并且依赖于 Fortran 编译器。R 在构建时检测 Fortran 编译器是否要使用 R 正在构建的参数,但它仅将 size_t 视为类型。GFortran 7 使用 int,但为了传递 1(实际上永远不会读取),并为了为被调用者提供一个“临时”空间以传递另一个永远不会读取的 1,使用更宽的类型是可以的。

这不仅比正确检测确切类型更简单,而且更安全,因为当 R 使用外部 BLAS/LAPACK 实现(不是 R 中包含的参考实现)时,我们无法控制如何构建该实现,并且选择是在运行时/动态链接时完成的。此外,经过优化的 BLAS/LAPACK 实现可能会用 C 中的自定义实现替换某些函数,而其他函数(通常来自 LAPACK)将重新使用参考 Fortran 实现,因此某些函数需要隐藏参数,而其他函数则不需要。由于当前编译器通常使用 64 位隐藏长度参数,因此 size_t 是更安全的选择。另请注意,在 64 位 CPU 上,即使对于 int 参数,通常也会使用 64 位松弛槽。

R 包中的更改

还应修复 R 包,以便在从 C 调用 Fortran 时提供隐藏长度参数。CRAN 现在使用 LTO 执行附加检查,在这些问题仍然存在时报告 LTO 类型不匹配,并要求包作者修复这些问题。许多包已经修复,有时在 CRAN 团队的帮助下修复。

包可以使用 R 在其代码中使用的相同新宏(FCONEFC_LEN_TUSE_FC_LEN_T)来修复对 BLAS 和 LAPACK 的调用。这在 编写 R 扩展 中有说明。这些宏还可以在出于某种原因选择包含其自己的 LAPACK 甚至 BLAS 版本的包中使用。

或者,包作者还可以使用 iso_c_binding(Fortran 2003)从 C 创建具有标准化调用约定的 Fortran 函数,如本博客的先前版本和现在在编写 R 扩展中所述。

从 Fortran 到 BLAS 和 LAPACK 的调用不受影响:隐藏长度参数将由 Fortran 编译器正确添加。为了安全起见,客户端代码和 BLAS/LAPACK 都需要由同一个 Fortran 编译器构建,但实际上许多编译器将兼容,并且 GFortran 将添加隐藏长度,如上所述,即使在不期望的情况下这样做似乎也是安全的。

我看到并调试了一个示例,其中一个包从 C(没有适当的长度)和 Fortran 调用 BLAS/LAPACK。LTO 类型不匹配警告似乎表明 Fortran 的调用是错误的,但事实并非如此,该警告是因为 C 中的调用不正确,但与没有隐藏长度参数的(旧的和不正确的)头文件匹配。

请注意,我们还考虑了该问题的替代解决方案,主要是寻找一些不需要修改调用 BLAS/LAPACK 的包的源代码的解决方案。我尝试了一种基于元编程技巧的 C 预处理器解决方案(从 C 预处理器中的 if-then-else 实现开始),该解决方案允许使用 F77_CALL 宏以 F77_CALL(foo)(x) 的通常形式自动重写一些调用,同时保持其他调用不变。这适用于简单的示例,但如果出现问题,对于包作者来说调试起来会很困难,而且远非简单。此外,一些包坚持包含自己的 LAPACK/BLAS 版本,无论如何都必须调整这些版本,可能需要更多工作。此外,如果(不太可能)将 BLAS/LAPACK 函数的名称重新用于其他用途,替代解决方案可能会产生令人惊讶的效果。R 中采用的解决方案虽然需要手动更新包,但故意非常简单。

Linux 发行版的更改

在 GCC 发布后,通常需要一段时间才能用于构建 Linux 发行版的二进制包。由于现在只有 9.x 系列具有 -ftail-call-workaround 的版本,并且此版本(9.2)相当新,因此不幸的是,Linux 发行版可能很快就会使用编译了激进优化的 BLAS/LAPACK 实现;不传递隐藏字符长度的应用程序,包括尚未修复的 R 包,预计会崩溃或工作不正确。这已经在使用 OpenBLAS 的 Fedora 30 中看到了。

Fedora 30 现在随附有 OpenBLAS,并带有此问题(包括 libRblas.so 替换、串行、OpenMP、线程在内的所有版本)。Fedora 29、Ubuntu 19.04、Ubuntu 18.04、Debian 10 和 Debian 9 中的 OpenBLAS 似乎正常。Fedora 30、Fedora 29、Ubuntu 19.04、Ubuntu 18.04、Debian 10 和 Debian 9 中的参考 (netlib) LAPACK 似乎正常。

不幸的是,这些观察结果可能会发生变化,短期内可能会变得更糟或更好。R 包的唯一防御措施是正确传递隐藏长度参数。不过,如果 Linux 发行版中的 BLAS/LAPACK 实现都以不需要长度的方式重新构建,那将是有意义的。