之前在搜狗曾经看过一篇技术文章,详细讲述了gcc编译器的一个优化bug,也就是在开-O2的时候,汇编指令不对。但是没往心里去,逐渐淡忘。
前一段时间申请了虚机,就在上面yum install gcc,默认提供的还是4.1.2版本的。这也造成了我的一个悲剧(因为这个bug折腾了半天)。
一般没有人会去怀疑编译器的,是吧?
进入正题,最近在写一个小的网络程序,用C++写server,用java写client,因为网络传输涉及到byte order的问题,一般是统一转成big endian。
我发现有htonl,htons(当然他们的能力是32bit和16bit,我需要的是64bit,所以自己实现了一个htond),但是我要的数据是double,不好做位操作,怎么办呢?一个极其简单的方法就是将其转为long,64位机器long和double都是8字节。
double v;
uint64_t x = *(uint64_t*)&v;
x = htond(x);
v = *(double*)&x;
我先用C++实现了一个客户端(本人对java不熟),但是接收到的数据和我传输的不一致。经过一步步打印,发现数据确实正确的传到了客户端,但是就是经过一个double-long-double的过程,数据打印出来就不对。我甚至怀疑,我的这个转换方法难道有错?
后来写了一个小实验程序,验证问题,发现开O2的时候,结果出错,如果是O0,结果正确。
int main() {
uint64_t z = 0x1234567890abcdef;
double x;
x = *(double*)&z;
uint64_t y = *(uint64_t*)&x;
printf( "%lx\n", y );
return 0;
}
这段程序,在O2下输出0,在O0下输出1234567890abcdef。
对比了一下这段程序在两种情况下的汇编代码:
O2 | O0 |
|
|
可以看到,O2的情况下,也还是很傻的从%rax》16(%rsp)》%rax的挪动数据,并且将未被赋值的8(%rsp)的值先放在了%rsi,然后才更新了8(%rsp)的值,
导致%rsi里面是一个随机值。O0情况下,立即数是分次装入的,但是执行次序没有错,所以结果正确。
从这里我们可以猜测,调用printf时,将format字符串放在%edi,将需要打印的数据放在%rsi,将%eax清0,然后call printf就行了。
后来我下载了gcc 4.4.6,然后同样用O2编译,产生了正确的并且更简短的代码,如下:
main:
.LFB12:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
subq $8, %rsp
.cfi_def_cfa_offset 16
movabsq $1311768467294899695, %rsi
movl $.LC0, %edi
xorl %eax, %eax
call printf
xorl %eax, %eax
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
所以,如果有还在用老编译器的兄弟,赶紧更新吧。