以下内容来自搜狗实验室技术交流文档,搜狐公司研发中心版权所有,仅供技术交流
 
摘要
---------
乱序优化是现代编译器非常重要的特性,本文介绍了什么是乱序优化,以及由此引发的一个bug,希望引起各位开发者的注意。
 
乱序优化
---------
乱序优化和cpu的乱序执行很类似。
现代cpu都采用流水线结构,流水线的各级可以同时执行不同的指令,也只有用多条指令将流水线填满以后,cpu的能力才能得到充分发挥。
乱序执行(out-of-order execution)是指cpu允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。这样将根据各个电路单元的状态和各指令能否提前执行的具体情况分析后,将能提前执行的指令立即发送给相应电路单元执行,在这期间不按规定顺序执行指令,然后由重新排列单元将各执行单元结果按指令顺序重新排列。采用乱序执行技术的目的是为了使cpu内部电路满负荷运转并相应提高了cpu的运行程序的速度。
由于cpu流水线的指令预取范围有限,所以只能在很小的范围内判断指令是否能够并发,如果相隔比较远的指令才可以并发就无能为力了。编译器可以分析相当长的一段代码从而把能够并发的代码经量靠近,这就是所谓的乱序优化。
乱序优化的关键在于编译器能够正确的识别哪些代码能够并发,如果发生误判就会导致不可预见的bug。一般情况下都没有问题--但是如果你写出一些太过诡异的代码就很难讲了。
 
来看一个优化错误实例程序:
buggy.c
#include <stdio.h>
#include <stdlib.h> typedef unsigned short UINT16;
typedef unsigned int UINT32; struct EndPoint{
UINT16 tcpPort_;
UINT16 udpPort_;
//UINT32 ipAddress_;
}; inline UINT32 EndPointToUInt32(struct EndPoint* ep){
return *(const UINT32*)(ep); //buug here
} struct EndPoint endpoint = {0x8080, 0x1080}; int main()
{
//下句在inline+乱序优化时出错
endpoint.udpPort_ = ; UINT32 tmp2 = EndPointToUInt32(&endpoint);
//UINT32 tmp2 = *(const UINT32*)(&endpoint); //用这一句替换上一句同样出错
srand(tmp2); // for break the optimize
printf("%08x %08x should be same as 00008080\n", tmp2, EndPointToUInt32(&endpoint));
}

运行结果如下:

gcc buggy.c

./a.out

00008080 00008080 should  be same as 00008080

gcc -O2 buggy.c

./a.out

10808080 00008080 should be same as 00008080

可以看到打开优化之后EndPointToUInt32这个函数的第一次执行就不正常了。

分析

---------

粗略的分析一下目标码

gcc直接编译的结果 替换掉函数调用后的结果 gcc -o2编译的结果
movw $0,endpoint+2 movw $0,endpoint+2 movl endpoint,%ebx
pushl $endpoint movl endpoint,%eax subl $28,%esp
call EndPiontToUInt32 movl %eax,-4(%ebp) pushl %ebx
addl $4,%esp subl $12,%esp movw $0,endpoint+2
movl %esx,-4(%ebp) pushl -4(%ebp) call srand
subl $12,%esp call srand addl $12,%esp
pushl -4(%ebp) addl $16,%esp pushl endpoint
call srand subl $4,%esp pushl %ebx
addl %16,%esp pushl $endpoint pushl $.LC0
subl $4,%esp call EndPointToUInt32 call printf
pushl $endpoint addl $4,%esp  
call EndPointToUInt32 pushl %eax  
addl $4,%esp pushl -4(%ebp)  
pushl %eax pushl $.LC0  
pushl -4(%ebp) call printf  
pushl $.LC0    
call printf    
     
     

左边的是优化之前的代码,然后movw置endpoint的一半为0,然后取出endpoint的地址调用EndPointToUInt32,并把结果放到tmp2也就是-4(%ebp)中。

中间的代码是将函数inline化以后的结果,注意到现在直接把endpoint的内容通过%eax传给了tmp2也就是-4(%ebp)

右边的代码经过了-o2优化,首先做了一次inline操作,取消了对EndPointToUInt32的调用,也就是直接把endpoint的内容作为EndPointToUInt32的返回值来处理。其次,取消了tmp2变量,用%ebx来替代。至此都没有问题。

问题在于将movw $0,endpoint+2一句优化到了movl endpoint, %ebx的后面。这里做了一个错误的乱序优化。这是因为首先gcc没有能够正确的判断出*(const UINT32*)(&endpoint)实际上和endpoint.udpPort_是相关的,从而优化出错。本来这也是可以容忍的,毕竟写法太变态。但是gcc又在处理inline时过于冒进,没有按照真正的函数调用那样在函数调用处设置一个边界,阻止函数调用前后的代码混杂,而是像一个宏展开一样简单的处理了,最后导致了和预想不一致的结果。

结论

---------

gcc除少数版本外,在-o2乱序优化时都不够完善,不能正确判断代码的影响范围,从而做出错误的乱序。所以请不要引入一些编译器难以判断影响范围的语句,尤其是胡乱cast。典型的如上面程序中的*(const UINT32*)(ep);

gcc的乱序优化对inline函数是像宏展开一样处理的,这可能导致将函数和函数附近的代码乱序,需要小心,常用的FC3/FC5上的gcc都有此问题。

乱序优化与GCC的bug的更多相关文章

  1. volatile关键字及编译器指令乱序总结

    本文简单介绍volatile关键字的使用,进而引出编译期间内存乱序的问题,并介绍了有效防止编译器内存乱序所带来的问题的解决方法,文中简单提了下CPU指令乱序的现象,但并没有深入讨论. 以下是我搭建的博 ...

  2. 【操作系统之十一】任务队列、CPU Load、指令乱序、指令屏障

    一.CPU Loadcpu load是对使用或者等待cpu进程的统计(数量的累加):每一个使用(running)或者等待(runnable)CPU的进程,都会使load值+1;每一个结束的进程,都会使 ...

  3. sort排序bug乱序

    项目需要对组件的zIndex值进行降序排列,刚开始采用的是sort进行排序,排完之后感觉没问题,毕竟也是经常用的,可是昨天无意中把zIndex值打出来看,一看不知道,发现只要排序的组件超过10个就出问 ...

  4. Chrome谷歌浏览器中js代码Array.sort排序的bug乱序解决办法

    [现象] 代码如下: var list = [{ n: "a", v: 1 }, { n: "b", v: 1 }, { n: "c", v ...

  5. memory barrier 内存屏障 编译器导致的乱序

    小结: 1. 很多时候,编译器和 CPU 引起内存乱序访问不会带来什么问题,但一些特殊情况下,程序逻辑的正确性依赖于内存访问顺序,这时候内存乱序访问会带来逻辑上的错误, 2. https://gith ...

  6. 由乱序播放说开了去-数组的打乱算法Fisher–Yates Shuffle

    之前用HTML5的Audio API写了个音乐频谱效果,再之后又加了个播放列表就成了个简单的播放器,其中弄了个功能是'Shuffle'也就是一般播放器都有的列表打乱功能,或者理解为随机播放. 但我觉得 ...

  7. 关于乱序(shuffle)与随机采样(sample)的一点探究

    最近一个月的时间,基本上都在加班加点的写业务,在写代码的时候,也遇到了一个有趣的问题,值得记录一下. 简单来说,需求是从一个字典(python dict)中随机选出K个满足条件的key.代码如下(py ...

  8. fastjson存在乱序的问题

    现象及原因 通常来讲,在使用json数据格式时一般不需要要求数据有序.但凡事都有例外,针对查询时序数据这样一个场景,就必须要求服务器端返回的数据是按时间有序的,否则前端在进行数据展示时就会有问题. 项 ...

  9. 【转】C 编译器优化过程中的 Bug

    C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...

随机推荐

  1. L2-014. 列车调度(set的使用,最长递增子序列)

    L2-014. 列车调度 时间限制 300 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 陈越 火车站的列车调度铁轨的结构如下图所示. Figure ...

  2. Java-API:java.util.ArrayList

    ylbtech-Java-API:java.util.ArrayList 1.返回顶部   2.返回顶部   3.返回顶部   4.返回顶部   5.返回顶部 0. https://docs.orac ...

  3. SQL 从身份证号得到出生日期、年龄、男女

    ), CONVERT(smalldatetime, SUBSTRING(b.IDCard, , )), ) AS BrithDate_Name, DATEDIFF(year, CONVERT(smal ...

  4. 1 ignite核心特性

    1 Ignite是什么? Apache Ignite是一个以内存为中心的分布式数据库.缓存和处理平台,支持事务.分析以及流式负载,可以在PB级数据上享有内存级的性能. 2 Ignite是不是内存数据库 ...

  5. Nginx简单入门教学,包学包会,让你不再依赖伪大神!

    这篇教程简单介绍了 nginx 并且讲解了一些 nginx 可以解决的简单任务.这里,我们假设 nginx 已经安装在读者的机器上.如果没有,可以看一下如何安装 nginx.这篇教程主要讲解的是如果启 ...

  6. No result defined for action action.LoginAction and result success 问题解决

    转自:https://blog.csdn.net/dongzhout/article/details/43699699 搭建好SSH2框架,写一个简单的登陆功能,提交表单的时候遇到这个问题: 配置文件 ...

  7. JS中,日期对象(获取当前现在的年份,星期,时间)

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  8. Java通过JDBC 进行Dao层的封装

    前言 前面有一章节,我专门讲解了Java通过JDBC 进行MySQL数据库操作,这主要讲解了MySQL数据库的连接和简单的操作,但是在真正的Java项目中,我们要不断的和数据库打交道,为了提高数据库操 ...

  9. DOM详习讲解

    http://www.cnblogs.com/wupeiqi/articles/5643298.html 

  10. linux ORACLE备份还原(EXP\IMP)

    一.Oracle导入导出 1.Oracle的备份是Oracle操作中常见的工作,常见的备份方案有:逻辑备份(IMP&EXP命令进行备份).物理文件备份(脱机及联机备份).利用RMAN(Reco ...