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
-2147483648
不会做你认为它做的事情.C没有负常数.包含limits.h
和使用INT_MIN
(INT_MIN
两个补充机器上的每个定义几乎都定义它是(-INT_MAX - 1)
有充分理由的).
A = ~A + 1;
调用未定义的行为,因为~A + 1
导致整数溢出.
它不是编译器,而是你的代码.
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编译器资源管理器检查程序集以进行各种编译器优化.
Lun*_*din 17
详细解释这里发生了什么:
在这个答案中,我假设它long
是32位并且long long
是64位.这是最常见的情况,但不能保证.
C没有符号整数.-2147483648
实际上是类型long long
,在其上应用一元减号运算符.
在检查是否2147483648
适合之后,编译器选择整数常量的类型:
int
?不,它不能.long
?不,它不能.long long
?是的,它可以.因此整数常量的类型将是long long
.然后在那上面应用一元减号long long
.long long
期望的函数显示这个否定int
.一个好的编译器可能在这里警告.您强制将隐式转换为较小的类型("左值转换").-2147483648
可以适合于a int
,因此转换不需要实现定义的行为,否则就是这种情况.下一个棘手的部分是negative
你使用的功能0x80000000
.这不是一个int
,也不是一个long long
,而是一个unsigned int
(请参阅此解释).
比较你传递int
给的时unsigned int
,"通常的算术转换"(见这个)强制隐式转换为int
to unsigned int
.它不会影响这种特定情况下的结果,但这就是为什么gcc -Wconversion
用户在这里得到一个很好的警告.
(提示:-Wconversion
已经启用!它可以捕获微妙的错误,但不是部分-Wall
或-Wextra
.)
接下来,您~A
将对值的二进制表示形式进行逐位反转,最后得到该值0x7FFFFFFF
.事实证明,这与INT_MAX
32或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
整数进行所有这些技巧,这是标准所要求的整数(并且具有明确定义的按位语义)操作,与有符号整数不同).
首先转换int
为unsigned
(根据标准定义,产生预期结果),做你的东西,转换回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 次 |
最近记录: |