提示:公众号展示代码会自动折行,建议横屏阅读

问题描述

前几天进行测试,发现一个神奇的现象:不加任何优化的版本与加了-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, 腾讯数据库技术团队专注于增强数据库内核功能,提升数据库性能,保证系统稳定性并解决用户在生产过程中遇到的问题,并对生产环境中遇到的问题及知识进行分享。

文章来源于腾讯云开发者社区,点击查看原文