提示:公众号展示代码会自动折行,建议横屏阅读
问题描述
前几天进行测试,发现一个神奇的现象:不加任何优化的版本与加了-O2参数的版本测试结果不一致!
主要代码类似以下例子:
#include <stdio.h>
#include <stdint.h>
class Foo {
public:
Foo() { printf("%s\n", "ctor"); }
~Foo() { printf("%s\n", "dtor"); }
struct tm {
unsigned int second : 10;
unsigned int minute : 10;
unsigned int hour : 10;
unsigned int unused : 2;
} _tm;
void set(short second) {
// zero out all of the bits.
*(int *)&_tm = 0;
_tm.second = second < 0 ? (second * -1) : second;
if (second < 0) {
// negative the value.
*(int *)&_tm = *(int *)&_tm * -1;
}
}
void output() {
int sign = 1;
if (*(int *)&_tm < 0) {
*(int *)&_tm = *(int *)&_tm * -1;
sign = -1;
}
printf("_tm = %d,", *(int *)&_tm);
printf("hour = %d,", _tm.hour);
printf("min = %d,", _tm.minute);
printf("sec = %d\n", _tm.second * sign);
}
};
int main(void) {
static_assert(sizeof(int) == 4, "sizeof(int) expect 4");
static_assert(sizeof(unsigned int) == 4, "sizeof(unsigned int) expect 4");
static_assert(sizeof(Foo::tm) == 4, "sizeof(tm) expect 4");
Foo foo;
short second = -1;
foo.set(second);
foo.output();
return 0;
}
不加-O2参数的时候,输出:
ctor
_tm = 1,hour = 0,min = 0,sec = -1
dtor
而加上-O2后,输出结果变为:
ctor
_tm = 1,hour = 0,min = 0,sec = 1
dtor
分析过程
这段代码很简单,就是输入一个短整数,取它的绝对值并存入结构体的低10位中,如果短整数是负的,将整个结构体作为一个整数取负值保存。读取的时候做对应转换把原值输出。
首先用gdb单步跟踪了一下,发现set执行完毕后存储的值为1。
(gdb) p foo._tm
$2 = {second = 1, minute = 0, hour = 0, unused = 0}
很明显此时应该是-1,即0xFFFFFFFF。用不带-O2参数的版本查看了一下,输出是正确的:
(gdb) p foo._tm
$2 = {second = 1023, minute = 1023, hour = 1023, unused = 3}
set函数为何执行出错了呢?继续进入set函数单步跟踪了下:
(gdb) s
set (second=-1, this=0x7fffffffe490) at test.cpp:23
23 _tm.second = second < 0 ? (second * -1) : second;
(gdb) n
22 *(int *)&_tm = 0;
(gdb)
23 _tm.second = second < 0 ? (second * -1) : second;
(gdb) p second
$1 = -1
(gdb) n
main () at test.cpp:50
50 foo.output();
看起来下面这段代码没执行啊,而且second已经显示是-1,应该是能进入判断里面执行的。
if (second < 0) {
// negative the value.
*(int *)&_tm = *(int *)&_tm * -1;
}
当然这里还有点问题,这里连
if (second < 0)
这一行代码都好像没有执行。不过优化过后的代码执行顺序已经变了,这个不一定就是真的没执行。没办法,只能查看下汇编代码了。
20 void set(short second) {
21 // zero out all of the bits.
22 *(int *)&_tm = 0;
0x0000000000400669 <+25>: movl $0x0,(%rsp)
23 _tm.second = second < 0 ? (second * -1) : second;
0x0000000000400664 <+20>: mov $0x1,%eax
0x0000000000400670 <+32>: mov %ax,(%rsp)
24 if (second < 0) {
25 // negative the value.
26 *(int *)&_tm = *(int *)&_tm * -1;
27 }
28 }
29
30 void output() {
31 int sign = 1;
0x0000000000400656 <+6>: mov $0x1,%ebx
真相终于浮出水面!汇编代码里面很明显把那两行关键代码给优化掉了。对比一下未加-O2参数的汇编代码:
20 void set(short second) {
0x00000000004007e0 <+0>: push %rbp
0x00000000004007e1 <+1>: mov %rsp,%rbp
0x00000000004007e4 <+4>: mov %rdi,-0x8(%rbp)
0x00000000004007e8 <+8>: mov %esi,%eax
0x00000000004007ea <+10>: mov %ax,-0xc(%rbp)
21 // zero out all of the bits.
22 *(int *)&_tm = 0;
0x00000000004007ee <+14>: mov -0x8(%rbp),%rax
0x00000000004007f2 <+18>: movl $0x0,(%rax)
23 _tm.second = second < 0 ? (second * -1) : second;
0x00000000004007f8 <+24>: movswl -0xc(%rbp),%eax
0x00000000004007fc <+28>: cltd
0x00000000004007fd <+29>: xor %edx,%eax
0x00000000004007ff <+31>: sub %edx,%eax
0x0000000000400801 <+33>: and $0x3ff,%ax
0x0000000000400805 <+37>: mov %eax,%edx
0x0000000000400807 <+39>: mov -0x8(%rbp),%rax
0x000000000040080b <+43>: mov %edx,%ecx
0x000000000040080d <+45>: and $0x3ff,%cx
0x0000000000400812 <+50>: movzwl (%rax),%edx
0x0000000000400815 <+53>: and $0xfc00,%dx
0x000000000040081a <+58>: or %ecx,%edx
0x000000000040081c <+60>: mov %dx,(%rax)
24 if (second < 0) {
0x000000000040081f <+63>: cmpw $0x0,-0xc(%rbp)
0x0000000000400824 <+68>: jns 0x400834 <Foo::set(short)+84>
25 // negative the value.
26 *(int *)&_tm = *(int *)&_tm * -1;
0x0000000000400826 <+70>: mov -0x8(%rbp),%rax
0x000000000040082a <+74>: mov -0x8(%rbp),%rdx
0x000000000040082e <+78>: mov (%rdx),%edx
0x0000000000400830 <+80>: neg %edx
0x0000000000400832 <+82>: mov %edx,(%rax)
27 }
28 }
0x0000000000400834 <+84>: pop %rbp
0x0000000000400835 <+85>: retq
但是问题又来了,为什么编译器会把这两行这么重要的代码给优化掉呢?查阅众多资料后依然没有找到答案,不禁怀疑编译器有bug。此时一篇编译器bug相关文章跳入眼里:技多不压身——从一个编译器的”bug”谈起。然后发现我们的情形与题主的案例非常相似,但是我们的代码并没有出现溢出现象,应该不是一个原因,不过也基本可以肯定我们的问题也来源于编译器的feature。不管三七二十一,先试试他的解决方案再说,试完发现文章提到的方案的确并不适用于我们的情况,接下来只好老老实实查看gcc文档:Options That Control Optimization。
终于我们发现了一个强相关的参数-fstrict-aliasing。我们来看看官方说明:
-fstrict-aliasing
Allow the compiler to assume the strictest aliasing rules applicable to the language being compiled. For C (and C++), this activates optimizations based on the type of expressions. In particular, an object of one type is assumed never to reside at the same address as an object of a different type, unless the types are almost the same. For example, an unsigned int can alias an int, but not a void* or a double. A character type may alias any other type.
很明显我们的代码违反了这个参数的假定条件,并且这个参数在-O2情况下是默认开启的。官方还举了个例子强调即使转换使用了联合类型,通过获取地址然后强转指针来进行的访问具有未定义的行为!具体到我们这个例子,以下代码进行了初始化
*(int *)&_tm = 0;
而-fstrict-aliasing参数假定了我们不会进行类型的转换来使用结构体,当编译器发现代码
*(int *)&_tm = *(int *)&_tm * -1;
时认为_tm未进行过其他赋值操作,这行代码就是0 * -1 = 0,直接就优化掉了,从而导致最后程序的执行结果不符合预期。
最后还有一个问题,这种强假设条件一般情况下是会报警告的,如下所示:
$ g++ -Wall -std=c++11 test.cpp -O2 -g -o neg
test.cpp: In member function 'void Foo::set(short int)':
test.cpp:23:14: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
*(int *)&_tm = 0;
为什么我们的代码编译的时候却没有报警?原来,因为代码不规范,编译时有好多告警,当初处理的时候添加了参数-Wno-strict-aliasing,这个参数仅仅是把警告给消除了,并没有取消这个假定条件的优化,正确的做法是使用参数-fno-strict-aliasing。至此,这个奇怪的问题彻底解决了。
腾讯数据库技术团队对内支持微信红包,彩票、数据银行等集团内部业务,对外为腾讯云提供各种数据库产品,如CDB、CTSDB、CKV、CMongo, 腾讯数据库技术团队专注于增强数据库内核功能,提升数据库性能,保证系统稳定性并解决用户在生产过程中遇到的问题,并对生产环境中遇到的问题及知识进行分享。