【问题标题】:How can I prevent the gcc optimizer from producing incorrect bit operations?如何防止 gcc 优化器产生不正确的位操作?
【发布时间】:2018-07-23 12:45:45
【问题描述】:

考虑以下程序。

#include <stdio.h>

int negative(int A) {
    return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~A + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}
int main(){
    divide(-2147483648, -1);
}

当它在没有编译器优化的情况下编译时,它会产生预期的结果。

gcc  -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

使用编译器优化编译时,会产生以下错误输出。

gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative 
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

我正在运行gcc version 5.4.0

我可以在源代码中进行更改以防止编译器在-O3 下产生这种行为吗?

【问题讨论】:

  • A = ~A + 1; 是 UB,如果 A == INT_MIN+1 会使有符号整数溢出。
  • 对于 32 位 int 类型,0x7FFFFFFF + 1 不是未定义行为吗?
  • @mch,我认为你一针见血。我从来没有想过有符号整数溢出的行为不像无符号整数溢出。我刚刚看到this question 所以现在我知道其中的区别了。
  • @Someprogrammerdude 不,它绝对以生成错误代码而闻名。当然,GCC 中存在编译器错误(一切都有错误),但-O3 是一个安全的默认设置,并且从根本上说,该设置中的影响正确性的错误并不比一般的 GCC 中更多。 stackoverflow.com/a/11546263/1968
  • @Voo 可以说这是似是而非。但是,除非有证据,否则在当前的编译器中根本不是这种情况(-O3 曾经是实验性的,因此存在缺陷;但已经很久没有了)。说它“已知有时会生成错误代码”是完全错误的。

标签: c gcc compiler-optimization


【解决方案1】:
  1. -2147483648 不会做你认为的那样。 C没有负常数。包括limits.h 并改用INT_MIN(几乎每个INT_MIN 在二进制补码机器上的定义都将其定义为(-INT_MAX - 1),这是有充分理由的)。

  2. A = ~A + 1; 调用未定义的行为,因为~A + 1 导致整数溢出。

不是编译器,而是你的代码。

【讨论】:

【解决方案2】:

编译器将您的 A = ~A + 1; 语句替换为单个 neg 指令,即此代码:

int just_negate(int A) {
    A = ~A + 1;
    return A;
}

将被编译为:

just_negate(int):
  mov eax, edi
  neg eax         // just negate the input parameter
  ret

但是编译器也足够聪明地意识到,如果A &amp; 0x80000000 在否定之前是非零的,那么它必须在否定之后是零,除非你依赖于未定义的行为。

这意味着第二个printf("negative(A) = %d\n", negative(A)); 可以“安全地”优化为:

mov edi, OFFSET FLAT:.LC0    // .string "negative(A) = %d\n"
xor eax, eax                 // just set eax to zero
call printf

我使用在线godbolt compiler explorer 来检查程序集的各种编译器优化。

【讨论】:

  • 这是对为什么编译器在高优化级别产生意外结果的有用解释。它不是在寻找在调用 UB 时欺骗程序员的方法;它正在寻找使代码运行得更快的方法,并假设没有 UB 这样做。
【解决方案3】:

详细解释这里发生了什么:

  • 在这个答案中,我假设 long 是 32 位,long long 是 64 位。这是最常见的情况,但不能保证。

  • C 没有带符号的整数内容。 -2147483648 实际上是 long long 类型,在其上应用一元减号运算符。

    编译器在检查2147483648 是否适合后选择整数常量的类型:

    • int 内?不,它不能。
    • long 内?不,它不能。
    • long long 内?是的,它可以。因此整数常量的类型将是long long。然后对该 long long 应用一元减号。
  • 然后你尝试将这个否定的long long 显示给一个期待int 的函数。一个好的编译器可能会在这里发出警告。您强制将隐式转换为更小的类型(“左值转换”)。
    但是,假设 2 的补码,值 -2147483648 可以放在 int 中,因此转换不需要实现定义的行为,否则会出现这种情况。
  • 下一个棘手的部分是使用0x80000000 的函数negative。这也不是int,也不是long long,而是unsigned int(解释一下see this)。

    在将传递的intunsigned int 进行比较时,“通常的算术转换”(see this) 会强制将int 隐式转换为unsigned int。在这种特定情况下,它不会影响结果,但这就是为什么gcc -Wconversion 用户会在这里得到一个很好的警告。

    (提示:已经启用-Wconversion!它有助于捕捉细微的错误,但不是-Wall-Wextra 的一部分。)

  • 接下来执行~A,这是值的二进制表示的按位反转,最终得到值0x7FFFFFFF。事实证明,这与 32 位或 64 位系统上的 INT_MAX 值相同。因此0x7FFFFFFF + 1 给出了一个有符号整数溢出,这会导致未定义的行为。这就是程序行为不端的原因。

    很高兴,我们可以将代码更改为 A = ~A + 1u;,然后突然一切都按预期工作了,这又是因为隐式整数提升。


经验教训:

在 C 中,整数常量以及隐式整数提升是非常危险且不直观的。他们可以巧妙地彻底改变程序的含义并引入错误。在 C 中的每一个操作中,您都需要考虑所涉及的操作数的实际类型。

使用 C11 _Generic 可能是查看实际类型的好方法。示例:

#define TYPE_SAFE(val, type) _Generic((val), type: val)
...
(void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
(void) TYPE_SAFE(0x80000000, int);  // won't compile, type is unsigned int

保护自己免受此类错误影响的良好安全措施是始终使用 stdint.h 并使用 MISRA-C。

【讨论】:

  • @Groo _Generic 是一个非常不错的功能,也是选择 C11 的唯一理由。显然还有一些关于它的ambiguous behavior,不同的编译器对标准的解释不同。希望这将在 Cxx 中得到修复。
【解决方案4】:

您依赖于未定义的行为。 0x7fffffff + 1 对于 32 位有符号整数会导致有符号整数溢出,根据标准,这是未定义的行为,所以一切正常。

在 gcc 中,您可以通过传递 -fwrapv 来强制环绕行为;尽管如此,如果您无法控制标志 - 更一般地说,如果您想要一个更可移植的程序 - 您应该对 unsigned 整数执行所有这些技巧,这是标准要求的环绕(并且已经明确定义)位运算的语义,不像有符号整数)。

首先将int 转换为unsigned(根据标准很好地定义,产生预期的结果),做你的事情,转换回int - 实现定义(≠未定义)的值大于int 的范围,但实际上是由每个使用 2 的补码来做“正确的事情”的编译器定义的。

int divide(int A, int B) {
    printf("A = %d\n", A);
    printf("negative(A) = %d\n", negative(A));
    if (negative(A)) {
        A = ~((unsigned)A) + 1;
        printf("A = %d\n", A);
        printf("negative(A) = %d\n", negative(A));
    }
    if (A < B) return 0;
    return 1;
}

您的版本(在 -O3 时):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0

我的版本(在-O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-12-11
    • 2021-11-23
    • 2014-10-04
    • 2023-03-03
    • 2011-01-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多