如何防止gcc优化器产生错误的位操作?

mer*_*011 28 c gcc compiler-optimization

考虑以下程序.

#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);
}
Run Code Online (Sandbox Code Playgroud)

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

gcc  -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1
Run Code Online (Sandbox Code Playgroud)

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

gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative 
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
Run Code Online (Sandbox Code Playgroud)

我运行gcc version 5.4.0.

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

Art*_*Art 83

  1. -2147483648不会做你认为它做的事情.C没有负常数.包含limits.h和使用INT_MIN(INT_MIN两个补充机器上的每个定义几乎都定义它是(-INT_MAX - 1)有充分理由的).

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

它不是编译器,而是你的代码.

  • 好的,我得问一下.发生了什么?谁与此有关?对于一个微不足道的问题,我得到了疯狂的投票数. (2认同)

Gro*_*roo 44

编译器用A = ~A + 1;一条neg指令替换你的语句,即这段代码:

int just_negate(int A) {
    A = ~A + 1;
    return A;
}
Run Code Online (Sandbox Code Playgroud)

将编译为:

just_negate(int):
  mov eax, edi
  neg eax         // just negate the input parameter
  ret
Run Code Online (Sandbox Code Playgroud)

但是编译器也足够聪明地意识到,如果A & 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
Run Code Online (Sandbox Code Playgroud)

我使用在线godbolt编译器资源管理器检查程序集以进行各种编译器优化.

  • 这是*为什么*编译器在高优化级别产生意外结果的有用解释.它不是在寻找程序员在调用UB时欺骗程序员的方法; 它正在寻找使代码运行更快的方法,并假设没有UB这样做. (18认同)

Lun*_*din 17

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

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

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

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

    • 在里面int?不,它不能.
    • 在里面long?不,它不能.
    • 在里面long long?是的,它可以.因此整数常量的类型将是long long.然后在那上面应用一元减号long long.
  • 然后你试着向long long期望的函数显示这个否定int.一个好的编译器可能在这里警告.您强制将隐式转换为较小的类型("左值转换").
    但是,假设2的补码,该值-2147483648可以适合于a int,因此转换不需要实现定义的行为,否则就是这种情况.
  • 下一个棘手的部分是negative你使用的功能0x80000000.这不是一个int,也不是一个long long,而是一个unsigned int(请参阅此解释).

    比较你传递int给的时unsigned int,"通常的算术转换"(见这个)强制隐式转换为intto unsigned int.它不会影响这种特定情况下的结果,但这就是为什么gcc -Wconversion用户在这里得到一个很好的警告.

    (提示:-Wconversion已经启用!它可以捕获微妙的错误,但不是部分-Wall-Wextra.)

  • 接下来,您~A将对值的二进制表示形式进行逐位反转,最后得到该值0x7FFFFFFF.事实证明,这与INT_MAX32或64位系统上的值相同.因此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
Run Code Online (Sandbox Code Playgroud)

保护自己免受这些错误的良好安全措施是始终使用stdint.h并使用MISRA-C.


Mat*_*lia 13

您依赖于未定义的行为.0x7fffffff + 1对于32位有符号整数导致有符号整数溢出,这是根据标准的未定义行为,所以任何事情都会发生.

在gcc中你可以通过传递强制环绕行为-fwrapv; 仍然,如果你无法控制标志 - 更一般地说,如果你想要一个更可移植的程序 - 你应该对unsigned整数进行所有这些技巧,这是标准所要求的整数(并且具有明确定义的按位语义)操作,与有符号整数不同).

首先转换intunsigned(根据标准定义,产生预期结果),做你的东西,转换回int- 实现定义(≠undefined)的值大于范围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;
}
Run Code Online (Sandbox Code Playgroud)

你的版本(在-O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
Run Code Online (Sandbox Code Playgroud)

我的版本(在-O3):

A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

4151 次

最近记录:

7 年,6 月 前