【问题标题】:Wrong result on modular arithmetic on ARM (Apple M1) with clang -O3 optimization带有clang -O3优化的ARM(Apple M1)上的模运算错误结果
【发布时间】:2021-07-14 02:24:27
【问题描述】:

在过去的几天里,我一直在用这段“无害”的代码(最小的可重现示例,更大的模乘例程的一部分):

#include <iostream>
#include <limits>

using ubigint = unsigned long long int;
using bigint = long long int;

void modmul(bigint a, bigint b, ubigint p) {
    ubigint ua = a < 0 ? -a : a;
    ubigint ub = b < 0 ? -b : b;

    ua %= p;
    ub %= p;

    std::cout << "ua: " << ua << '\n';
}

int main() {
    bigint minbigint = std::numeric_limits<bigint>::min();
    bigint maxbigint = std::numeric_limits<bigint>::max();
    std::cout << "minbigint: " << minbigint << '\n';
    std::cout << "maxbigint:  " << maxbigint << '\n';

    modmul(minbigint, maxbigint, 2314); // expect ua: 2036, got ua: 0
}

我正在使用从 Homebrew 安装的 clang 12.0 在 macOS 11.4 上进行编译

clang version 12.0.0 
Target: arm64-apple-darwin20.5.0 
Thread model:posix 
InstalledDir: /opt/homebrew/opt/llvm/bin

当使用clang -O1 编译时,程序会输出预期的结果(在本例中,2036,我使用Wolfram MathematicaMod[9223372036854775808, 2314] 进行了检查,这是正确的)。但是,当我使用clang -O2clang -O3(完全优化)编译时,不知何故变量ua 被清零(它的值变为0)。我在这里完全不知所措,不知道为什么会发生这种情况。 IMO,这段代码中没有UB,也没有溢出或任何可疑的东西。非常感谢任何建议,或者您是否可以在您身边重现该问题。

PS:代码在任何其他平台(包括 Windows/Linux/FreeBSD/Solaris)上的行为都符合预期,可以使用任何编译器组合。我只在带有 clang 12 的 Apple M1 上收到此错误(未在 M1 上使用其他编译器进行测试)。

【问题讨论】:

  • 好吧,你说没有溢出,但否定最负面的 bigint 有点阴暗。如果您在 否定之前转换为 ubigint,这仍然会发生吗? (否定无符号整数毕竟是安全的)
  • @harold Harold,你是金子!这似乎确实是问题所在。你可以发布一个答案吗?这很可能是 C(由 C++ 继承)的那些非常黑暗的角落之一。坦率地说,我不知道这是否是 UB……我想知道我是否会完全理解无符号与 C++ 中的有符号。
  • @harold 有趣的是即使使用std::abs 也会给出错误的答案,即ubigint ua = std::abs(a)a==minbigint
  • @vsoftco 使用-fsanitize=undefined,您会立即看到错误godbolt.org/z/b49zx1dKa。在这种情况下,您也可以使用静态分析器
  • @phuclv 谢谢!我实际上正在使用它,clang++ -fsanitize=undefined -O3 -Wall -Wextra test.cpp,不幸的是,我的平台上没有收到任何警告。

标签: c++ clang++ apple-m1 integer-arithmetic unsigned-long-long-int


【解决方案1】:

更新:正如@harold 在评论部分指出的那样,negqsubq 从 0 开始是完全一样的。所以我下面关于negqsubq 的讨论是不正确的。请忽略该部分,抱歉在发布答案之前没有仔细检查。

关于原来的问题,我重新编译了一个稍微简单一点的代码godbolt,发现有问题的编译器优化在main而不是modmul。在main 中,clang 看到modmul 的所有操作数都是常量,因此它决定在编译时计算modmul。在计算ubigint ua = a &lt; 0 ? -a : a; 时,clang 发现这是有符号整数溢出 UB,所以它决定返回 0 并打印出来。这似乎是一件激进的事情,但由于 UB,它是合法的。此外,由于二进制补码系统的限制,没有数学上正确的答案,因此返回 0 可以说与任何其他结果一样好(或一样坏)。


以下是旧答案

正如评论部分中有人指出的那样,您代码中的以下 2 行是未定义的行为 - 有符号整数溢出 UB。

    ubigint ua = a < 0 ? -a : a;
    ubigint ub = b < 0 ? -b : b;

如果您想知道 clang 在 2 个不同优化级别产生 2 个不同结果的幕后究竟是什么,请考虑一个简单的示例,如下所示。

using ubigint = unsigned long long int;
using bigint = long long int;

ubigint
negate(bigint a)
{
    ubigint ua = -a;
    return ua;
}

使用 -O0 编译时

negate(long long):                             # @negate(long long)
        pushq   %rbp
        movq    %rsp, %rbp
        movq    %rdi, -8(%rbp)
        xorl    %eax, %eax
        subq    -8(%rbp), %rax  # Negation is performed here
        movq    %rax, -16(%rbp)
        movq    -16(%rbp), %rax
        popq    %rbp
        retq

使用 -O3 编译

negate(long long):                             # @negate(long long)
        movq    %rdi, %rax
        negq    %rax  # Negation is performed here
        retq

在 -O0 处,clang 使用普通的 subq 指令执行 0 和 %rax 的二进制减法并产生具有整数环绕行为的结果。

在 -O3 处,clang 可以做得更好,它使用 negq 指令,该指令仅用其二进制补码替换操作数(即翻转所有位并加 1)。但是,您可以看到这种优化仅在有符号整数溢出是未定义行为时才合法(因此编译器可以忽略溢出情况)。如果标准要求整数环绕行为,clang 必须回退到未优化的版本。

【讨论】:

  • 这对我来说毫无意义。减去 0 和 neg完全相同的东西,而不是仅在条件下有效的东西。 neg 在最大负整数的情况下也可以。
  • @harold 在仔细检查了我的答案之后,我认为你是对的。我与上述negqsubq 相关的讨论不正确。关于原始问题,我尝试重新编译代码godbolt 的更简单版本,发现有问题的优化不在modmul 函数中,而是在main 中。在这里,clang 进行不断的传播,找到 UB 并决定根本不调用modmult。我认为这就是为什么在 -O3 处产生 0 的原因。你怎么看?
  • 可能就是这样,因为否定可能“出错”的唯一方法是它不会在运行时发生(任何合理的实现方式也适用于最负整数)
  • @harold:谢谢。我会更新我的答案。
猜你喜欢
  • 1970-01-01
  • 2022-08-18
  • 1970-01-01
  • 2022-02-09
  • 2021-04-17
  • 1970-01-01
  • 2022-07-19
  • 2021-06-14
  • 1970-01-01
相关资源
最近更新 更多