简单的 C 程序在 clang/macOS/arm64 和 clang/macOS/x86_64 中产生不同的结果

Sté*_*let 4 c macos clang arm64

我在 macOS/arm64 下移植一些复杂的东西时遇到了一些问题,最终得到了以下简单的代码来展示 macOS/x86_64 的不同行为(使用来自 conda-forge 的本机 osx/arm64 clang 版本 14.0.6,并针对 x86_64 进行交叉编译):

#include "assert.h"
#include "stdio.h"
int main()
{
    double y[2] = {-0.01,0.9};
    double r;
    r = y[0]+0.03*y[1];
    printf("r = %24.26e\n",r);
    assert(r == 0.017);
}
Run Code Online (Sandbox Code Playgroud)

在arm64上的结果是

$ clang -arch arm64 test.c -o test; ./test
Assertion failed: (r == 0.017), function main, file test.c, line 9.
r = 1.69999999999999977517983751e-02
zsh: abort      ./test
Run Code Online (Sandbox Code Playgroud)

而 x86_64 上的结果是

$ clang -arch x86_64 test.c -o test; ./test
r = 1.70000000000000012212453271e-02
$       
Run Code Online (Sandbox Code Playgroud)

测试程序也在 x86_64 机器上编译/运行,它产生与上面相同的结果(在 arm64 上交叉编译并使用 Rosetta 运行)。

事实上,arm64 结果不按位等于 1.7 解析并存储为 IEEE754 数字并不重要,而是表达式 wrt x86_64 的不同值。

更新1:

为了检查最终不同的约定(例如舍入模式),以下程序已在两个平台上编译并运行

#include <iostream>
#include <limits>

#define LOG(x) std::cout << #x " = " << x << '\n'

int main()
{
    using l = std::numeric_limits<double>;
    LOG(l::digits);
    LOG(l::round_style);
    LOG(l::epsilon());
    LOG(l::min());

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

它产生相同的结果:

l::digits = 53
l::round_style = 1
l::epsilon() = 2.22045e-16
l::min() = 2.22507e-308
Run Code Online (Sandbox Code Playgroud)

因此问题似乎出在其他地方。

更新2:

{1,0.03}如果它可以帮助:在arm64下,使用表达式获得的结果与使用向量和调用refBLAS ddot获得的结果相同y

更新3:

工具链似乎是原因。使用macOS 11.6.1的默认工具链:

mottelet@portmottelet-cr-1 ~ % clang -v
Apple clang version 13.0.0 (clang-1300.0.29.30)
Target: arm64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Run Code Online (Sandbox Code Playgroud)

两种架构给出相同的结果!所以问题似乎出在我使用的实际工具链中:我使用 conda 包的 1.5.2 版本cxx-compiler(我需要 conda 作为包管理器,因为我正在构建的应用程序有很多 conda 为我提供的依赖项)。

使用-v显示了一堆编译标志,哪一个最终会受到指控?

Mar*_*ler 5

由于给定编译器和体系结构的舍入不同,结果在最低有效位上有所不同。您可以使用%a十六进制查看双精度中的所有位。然后你就可以上arm64了:

0x1.16872b020c49bp-6

在 x86_64 上:

0x1.16872b020c49cp-6

IEEE 754 标准本身并不能保证一致的实现具有完全相同的结果,特别是由于目标精度、十进制转换和指令选择。最低有效位的变化,或者多次操作的变化,是可以而且应该预料到的。

在本例中,fmadd使用了arm64架构上的运算,在单个运算中完成乘法和加法。这给出了与 x86_64 架构中使用的单独乘法和加法 XMM 运算不同的结果。

在评论中,Eric 指出 C 库函数fma()可以进行组合乘加运算。事实上,如果我在 x86_64 架构(以及 arm64)上使用该调用,我会得到 arm64fmadd结果。

如果编译器优化了该操作,那么您可能会在同一体系结构中获得不同的行为,正如示例中所示。然后编译器进行计算。编译器可以在编译时很好地使用单独的乘法和加法运算,从而在arm64上给出与fmadd未优化时的运算不同的结果。此外,如果您正在进行交叉编译,那么优化后的计算可能取决于您正在编译的机器的体系结构,而不是您正在运行它的机器的体系结构。

比较浮点值的精确相等充满了危险。每当你看到自己尝试这样做时,你就需要更深入地思考你的意图。