先来看一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <cstdio> void exchange(int input, int* output) { short* pi = (short*)&input; short* po = (short*)output; po[1] = pi[0]; po[0] = pi[1]; } int main() { int input = 0xffff0000; printf("input : 0x%08x\n", input); int output = 0xababbaba; exchange(input, &output); printf("output : 0x%08x\n", output); return 0; } |
你觉得程序的输出是什么样的呢?
代码很容易理解,只做了一件事情,把 input变量储存的32位整数的高16位和低16位交换,存放在 output变量中,并输出这两个变量。
相信很多人都写过这样的代码(至少我写过),虽然觉得有点怪,但应该不会有什么问题,于是编译运行:
1 2 3 4 |
[kongfy@kongfy-vps dev]$ g++ -g test.cpp -o test [kongfy@kongfy-vps dev]$ ./test input : 0xffff0000 output : 0x0000ffff |
结果完全符合预期,似乎没有任何问题啊!
别高兴的太早,让我们试试打开编译器优化选项 -O2重新编译运行:
1 2 3 4 |
[kongfy@kongfy-vps dev]$ g++ -O2 -g test.cpp -o test [kongfy@kongfy-vps dev]$ ./test input : 0xffff0000 output : 0xababbaba |
问题出现了, output变量居然不符合预期?什么鬼?!
PS:似乎有必要注明用来测试的g++版本,我测试用的版本是4.4.7,经测试高版本g++得到的结果会不同,但这个问题仍然是存在的。
strict-aliasing
难道是编译器优化有问题么?但怀着对GNU的景仰…不,一定是我的使用方式有问题!
编译出现诡异问题怎么办?不要忘了使用 -Wall,编译器会给你线索:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[kongfy@kongfy-vps dev]$ g++ -Wall -O2 -g test.cpp -o test test.cpp: In function ‘void exchange(int, int*)’: test.cpp:7: warning: dereferencing pointer ‘pi’ does break strict-aliasing rules test.cpp:5: note: initialized from here test.cpp:8: warning: dereferencing pointer ‘<anonymous>’ does break strict-aliasing rules test.cpp:8: note: initialized from here test.cpp: In function ‘int main()’: test.cpp:8: warning: dereferencing pointer ‘po’ does break strict-aliasing rules test.cpp:6: note: initialized from here test.cpp:7: warning: dereferencing pointer ‘pi’ does break strict-aliasing rules test.cpp:5: note: initialized from here test.cpp:7: warning: dereferencing pointer ‘<anonymous>’ does break strict-aliasing rules test.cpp:7: note: initialized from here test.cpp:8: warning: dereferencing pointer ‘<anonymous>’ does break strict-aliasing rules test.cpp:8: note: initialized from here |
编译器果然给出了警告:我们的指针操作破坏了strict-aliasing规则,新的问题来了,什么是strict-aliasing?严格别名(非准确翻译)?
在 man g++中找到这样一段介绍:
1 2 3 4 5 6 |
-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. ... The -fstrict-aliasing option is enabled at levels -O2, -O3, -Os. |
简单来说,如果在编译器中开启了 -fstrict-aliasing选项( -O2优化级别默认开启这个选项),编译器会在“不同类型的变量一定存放在不同的内存空间中”的假定条件下对代码进行优化。
这实际是一个普通程序员和编译优化器编写者之间的约定:为了方便编译优化器的编写者写出更好的编译器优化功能,普通程序员在编写代码时要遵循这样的约定:“不同类型的变量一定存放在不同的内存空间中”。
反过来看看我们代码中的指针 pi和 po,他们是 short *类型,但他们指向的内存空间实际上是 int类型的 input变量,这就违反了strict-aliasing规则,但在开启 -O2优化时我们却告诉编译优化器我们遵守了strict-aliasing规则(默认开启),导致编译器做出了“错误”的优化。
来点汇编
明白了问题出现的原因,不妨看看编译器最终生成的汇编代码是怎样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
080484a0 <main>: 80484a0: 55 push %ebp 80484a1: 89 e5 mov %esp,%ebp 80484a3: 83 e4 f0 and $0xfffffff0,%esp 80484a6: 83 ec 20 sub $0x20,%esp 80484a9: c7 44 24 04 00 00 ff movl $0xffff0000,0x4(%esp) 80484b0: ff 80484b1: c7 04 24 b4 85 04 08 movl $0x80485b4,(%esp) 80484b8: e8 e3 fe ff ff call 80483a0 <printf@plt> 80484bd: 0f b7 44 24 18 movzwl 0x18(%esp),%eax 80484c2: c7 44 24 04 ba ba ab movl $0xababbaba,0x4(%esp) 80484c9: ab 80484ca: c7 04 24 c5 85 04 08 movl $0x80485c5,(%esp) 80484d1: 66 89 44 24 1e mov %ax,0x1e(%esp) 80484d6: 0f b7 44 24 1a movzwl 0x1a(%esp),%eax 80484db: 66 89 44 24 1c mov %ax,0x1c(%esp) 80484e0: e8 bb fe ff ff call 80483a0 <printf@plt> 80484e5: 31 c0 xor %eax,%eax 80484e7: c9 leave 80484e8: c3 ret |
看不懂是正常的… -O2级别的优化已经把代码搞的乱七八糟了。 main函数中没有调用 exchange函数的部分,进行了inline优化。
重要的是在调用第二个 printf函数的传参部分,可以看到 0x4(%esp)被直接赋予了 $0xababbaba并且没有修改过。
让我们尝试从编译器的角度思考我们的代码:函数 exchange中修改的内存都是 short类型的,既然程序员承诺遵循strict-aliasing规则,那么函数 exchange就不会修改 int类型的变量 output,所以可以优化一下直接输出初始值就可以了。
好聪明的编译器!好悲剧的程序猿…
怎么办?
那么这样的问题该如何避免呢?显然的,如果你告诉编译器遵循strict-aliasing规则,那在写代码的过程中就不应该尝试去打破这样的规则。但是我们在写C/C++代码的过程中常常需要编写这样一些打破规则的trick代码,与其让自己不自在,不如在编译时不要和编译器做这样的约定(使用 -fno-strict-aliasing编译参数),虽然不能让编译器做一些更加高效的优化,但安全总是第一位的,不是么?