用户名  找回密码
 FreeOZ用户注册
查看: 2375|回复: 18
打印 上一主题 下一主题

[论坛技术] 简单测试GCC编译器的优化情况

[复制链接]
跳转到指定楼层
1#
发表于 16-5-2009 01:07:40 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?FreeOZ用户注册

x
写了一个链接数据结构,程序及相关文件如附件。附件中包括:

1. oneway_link.h 头文件
2. oneway_link.cpp 实现部分
3. oneway_link_test.cpp 一个简单的测试程序
4. Makefile 简单的Makefile
5. oneway_test_link.s 没有优化时的编译结果(汇编代码)
6. oneway_test_link_opt.s 采用-O2优化后的编译结果

我重要分析的代码是这一段:
oneway_link.h
  1. 16 template<class T>
  2. 17 class MyLinkNode
  3. 18 {
  4. 21 public:
  5. 35     MyLinkNode * removeNext();
  6. 36 };

  7. 38 template <class T> class MyLink
  8. 39 {
  9. 40     MyLinkNode<T> * m_pHead;
  10. 41     MyLinkNode<T> * m_pTail;  //for appending method
  11. 46 public:
  12. 48     ~MyLink();
  13. 64 };
复制代码
oneway_link.cpp
  1. 58 template<class T> inline MyLinkNode<T> * MyLinkNode<T>::removeNext()
  2. 59 {
  3. 60     if(m_pNext==NULL)
  4. 61         return NULL;
  5. 62
  6. 63     MyLinkNode * pNextNext = m_pNext->m_pNext;
  7. 64     m_pNext->m_pNext = NULL;
  8. 65
  9. 66     MyLinkNode * pRetNext = m_pNext;
  10. 67     m_pNext = pNextNext;
  11. 68
  12. 69     return pRetNext;
  13. 70 }

  14. 75 template<class T> MyLink<T>::~MyLink()
  15. 76 {
  16. 77     if(m_pHead == NULL)
  17. 78         return;
  18. 79
  19. 80     MyLinkNode<T> * pNext;
  20. 81
  21. 82     while((pNext = m_pHead->removeNext())!=NULL)
  22. 83     {
  23. 84         delete pNext;
  24. 85     }
  25. 86
  26. 87     delete m_pHead;
  27. 88 }
复制代码
很显然,我用了比较“累赘”的方法来实现链接的删除。原因是我没有让MyLink成为MyLinkNode的友元,
所以MyLink不能直接操作MyLinkNode,这种情况下,他只能通过MyLinkNode::removeNext()这个接口
来实现逐个元素的删除。

如果我采用了friend的方式来写代码,一个比较“优化”的实现可能是:
  1. pNext = m_pHead->m_pNext;

  2. while(pNext!=NULL;)
  3. {
  4.    pNext2 = pNext->pNext;   
  5.    delete pNext;
  6.    pNext = pNext2;         
  7. }
复制代码
上面的这段代码与原来的代码最大的不同包括两点:
1. 没有采用函数调用的方式来获取下一个元素
2. 没有设置获取的元素的next指针值为NULL
3. 没有把原有的链表重构起来

由于我采用了inline的方式来实现代码,第1点自然可以被优化掉,关键是看第2/3两点。
不过,老实说,2和3两点都是很小的代码,是否有需要优化掉也是一个问题。姑且看看吧。

如果我没有搞错,下面这段汇编应该就是MyLink::~MyLink()的析构代码的优化结果(从*_opt.s从取出)
  1. 350     .weak   _ZN6MyLinkISsED1Ev
  2. 351     .type   _ZN6MyLinkISsED1Ev, @function
  3. 352 _ZN6MyLinkISsED1Ev:
  4. 353 .LFB1458:
  5. 354     pushl   %ebp                     
  6. 355 .LCFI30:
  7. 356     movl    %esp, %ebp
  8. 357 .LCFI31:
  9. 358     pushl   %esi
  10. 359 .LCFI32:
  11. 360     pushl   %ebx
  12. 361 .LCFI33:
  13. 362     subl    $16, %esp         
  14. 363 .LCFI34:
  15. 364     movl    8(%ebp), %esi
  16. 365     movl    (%esi), %ecx            这个地方备份%ecx的值,这个是m_pHead的值
  17. 366     testl   %ecx, %ecx
  18. 367     jne .L57                        #跳到While循环判断处
  19. 368     jmp .L53                        #函数返回
  20. 369     .p2align 4,,7
  21. 370 .L56:                               #While循环体
  22. 371     movl    4(%edx), %eax           %eax = %edx指向的对象的->m_pNext
  23. 372     movl    $0, 4(%edx)             %edx指向的对象->m_pNext设置为NULL,$0是不是0我不是太有把握
  24. 373     movl    4(%ecx), %ebx           %ebx=%ecx指向的对象->m_pNext
  25. 374     movl    %eax, 4(%ecx)           %ecx指向的对象->m_pNext改为%eax
  26. 375     testl   %ebx, %ebx          测试%ebx是否0值
  27. 376     je  .L53               While循环break出来,似乎对pNext!=NULL做了两个判断
  28. 377     movl    %ebx, (%esp)
  29. 378     call    _ZN10MyLinkNodeISsED1Ev    调用MyLinkNode的析构函数
  30. 379     movl    %ebx, (%esp)
  31. 380     call    _ZdlPv               调用free()之类的函数
  32. 381     movl    (%esi), %ecx         [从%esi备份中取出m_pHead放回%ecx中]
  33. 382 .L57:                            #.L57 while循环判断
  34. 383     movl    4(%ecx), %edx        [%ecx指向m_pHead,所以这里是m_pHead->next放到%edx中去]
  35. 384     testl   %edx, %edx           384/385两行用来测试%edx是不是0值,对应while循环中的pNext!=NULL语句
  36. 385     jne .L56
  37. 386 .L53:                            #.L53 函数出口
  38. 387     addl    $16, %esp
  39. 388     popl    %ebx
  40. 389     popl    %esi
  41. 390     popl    %ebp
  42. 391     ret
复制代码
这里重点是while循环的优化。这个While循环对应的是370至385这段汇编。
通过汇编代码的分析,这个优化后的while循否可以写成:
  1. do {
  2.   pNextNext = m_pNext->m_pNext;      //oneway_link.cpp: 63
  3.   m_pNext->m_pNext = NULL;           //oneway_link.cpp: 64
  4.   pRetNext = m_pHead->m_pNext;       //相当于oneway_link.cpp: 66, pRetNext = m_pNext;
  5.   m_pHead->next = pNextNext;         //相当于oneway_link.cpp: 67, m_pNext = pNextNext;
  6.   if(py == NULL)         //这个操作应该是来自MyLinkNode::removeNext():60行if(m_pNext==NULL)的代码的优化结果
  7.      break;
  8.   delete py;
  9. }while((pNode = m_pHead->next)!=NULL)
复制代码
从分析的结果看,我想看到的第2、3点优化并没有做。编译结果除了跟据inline去掉了相应的函数调用。
我试着把优化级别由-O2一直升至-O5,那5行while循环代码学是没有改变。

结论:
  编译器并没有象我想象那样“智能”地优化代码,基本上还是忠实于原来的实现。
  所以,如果很需要注重性能的话,还是有必要用友元的方式重写相应的程序

[ 本帖最后由 key 于 16-5-2009 01:11 编辑 ]

oneway_link.tar.gz

7.32 KB, 下载次数: 1

评分

参与人数 1威望 +30 收起 理由
ubuntuhk + 30 你太有才了!

查看全部评分

回复  

举报

2#
发表于 16-5-2009 01:29:05 | 只看该作者

回复 #1 key 的帖子

厉害,这年头已经很少有人通过分析汇编代码来进行代码优化了,这个例子非常清晰
回复  

举报

3#
发表于 16-5-2009 09:47:37 | 只看该作者
编译器不是不可能帮你自动"优化"第2,3点的,做不做第2,3点很可能对应着程序逻辑的变化,编译器再优化也不会去改变程序逻辑。况且编译器都是采用窥孔优化,所谓管中窥豹,所看到的汇编代码的context都是很小的。
回复  

举报

4#
发表于 16-5-2009 11:12:33 | 只看该作者
这个不是编译器应该干的事情吧。。。
回复  

举报

5#
 楼主| 发表于 16-5-2009 15:56:22 | 只看该作者
原帖由 coredump 于 16-5-2009 09:47 发表
编译器不是不可能帮你自动"优化"第2,3点的,做不做第2,3点很可能对应着程序逻辑的变化,编译器再优化也不会去改变程序逻辑。况且编译器都是采用窥孔优化,所谓管中窥豹,所看到的汇编代码的context都是很小的。


谢谢core同学的指点。

但我对于你关于“编译器都是采用“窥孔优化”这样的说法有所保留。通过查看gcc/g++的man,
我可以看到有大量的优化选项:
  1.        Optimization Options
  2.            -falign-functions=n  -falign-jumps=n -falign-labels=n  -falign-loops=n -fbounds-check -fmudflap -fmudflapth
  3.            -fmudflapir -fbranch-probabilities -fprofile-values -fvpt -fbranch-target-load-optimize -fbranch-tar-
  4.            get-load-optimize2 -fbtr-bb-exclusive -fcaller-saves  -fcprop-registers  -fcse-follow-jumps
  5.            -fcse-skip-blocks  -fcx-limited-range  -fdata-sections -fdelayed-branch  -fdelete-null-pointer-checks
  6.            -fearly-inlining -fexpensive-optimizations  -ffast-math  -ffloat-store -fforce-addr  -ffunction-sections
  7.            -fgcse  -fgcse-lm  -fgcse-sm  -fgcse-las  -fgcse-after-reload -floop-optimize -fcrossjumping  -fif-conver-
  8.            sion  -fif-conversion2 -finline-functions  -finline-functions-called-once -finline-limit=n
  9.            -fkeep-inline-functions -fkeep-static-consts  -fmerge-constants  -fmerge-all-constants -fmodulo-sched
  10.            -fno-branch-count-reg -fno-default-inline  -fno-defer-pop -floop-optimize2 -fmove-loop-invariants
  11.            -fno-function-cse  -fno-guess-branch-probability -fno-inline  -fno-math-errno  -fno-peephole  -fno-peep-
  12.            hole2 -funsafe-math-optimizations  -funsafe-loop-optimizations  -ffinite-math-only -fno-trapping-math
  13.            -fno-zero-initialized-in-bss -fomit-frame-pointer  -foptimize-register-move -foptimize-sibling-calls
  14.            -fprefetch-loop-arrays -fprofile-generate -fprofile-use -fregmove  -frename-registers -freorder-blocks
  15.            -freorder-blocks-and-partition -freorder-functions -frerun-cse-after-loop  -frerun-loop-opt -frounding-math
  16.            -fschedule-insns  -fschedule-insns2 -fno-sched-interblock  -fno-sched-spec  -fsched-spec-load
  17.            -fsched-spec-load-dangerous -fsched-stalled-insns=n -fsched-stalled-insns-dep=n -fsched2-use-superblocks
  18.            -fsched2-use-traces -freschedule-modulo-scheduled-loops -fsignaling-nans -fsingle-precision-constant
  19.            -fstack-protector  -fstack-protector-all -fstrength-reduce  -fstrict-aliasing  -ftracer  -fthread-jumps
  20.            -funroll-all-loops  -funroll-loops  -fpeel-loops -fsplit-ivs-in-unroller -funswitch-loops -fvariable-expan-
  21.            sion-in-unroller -ftree-pre  -ftree-ccp  -ftree-dce -ftree-loop-optimize -ftree-loop-linear -ftree-loop-im
  22.            -ftree-loop-ivcanon -fivopts -ftree-dominator-opts -ftree-dse -ftree-copyrename -ftree-sink -ftree-ch
  23.            -ftree-sra -ftree-ter -ftree-lrs -ftree-fre -ftree-vectorize -ftree-vect-loop-version -ftree-salias -fweb
  24.            -ftree-copy-prop -ftree-store-ccp -ftree-store-copy-prop -fwhole-program --param name=value -O  -O0  -O1
  25.            -O2  -O3  -Os
复制代码
其中之二是-fpeephole,-fpeephole2,前者被缺省enabled,后者则在-O2/-O3/-Os被enabled,可以通过-fno-peephole/-fno-peephole2来disable

至于你说,程序逻辑不能被改变,这个是当然了;但我觉得我期望的改变并不是程序逻辑。而我觉得阻止的编译器进行进一步优化的原因可能有两点:
1. 编译器不知道free()之后那个pNode->next指针是没有用的,也就是说汇编的372行可以删掉。
2. 编译器不知道Head->next->next与pNode->next的关系,在编译器手上有pNode的值的情况下,它还是坚持从备份堆栈读入Head指针值(汇编371,381,383行)
3. 编译器因为坚持要从Head->next读入数据进行下一步的循环(汇编381,383),所以就得坚持把链表补全(汇编374)
4. 编译器不知道Head->next与pNode的关系,所以动用了两个寄存器来处理同一个指针(汇编373)

其中第1点涉及到函数调用以及很多实际情况,基本上什么编译器都不可能做了。但第2-4点应该是可以做的,因为汇编368行用了绝对跳转,
所以.L56开始的基本块的数据定义是清晰和一致的。我只是不知道怎样引导编译器去“知道”上面说到的事实。

评分

参与人数 1威望 +30 收起 理由
coredump + 30 谢谢分享!

查看全部评分

回复  

举报

6#
发表于 18-5-2009 12:23:19 | 只看该作者
原帖由 ubuntuhk 于 16-5-2009 01:29 发表
厉害,这年头已经很少有人通过分析汇编代码来进行代码优化了,这个例子非常清晰



我们搞单片机的天天都得这么干啊
回复  

举报

7#
 楼主| 发表于 18-5-2009 14:31:21 | 只看该作者
原帖由 四香油饼 于 18-5-2009 12:23 发表



我们搞单片机的天天都得这么干啊

回复  

举报

8#
发表于 18-5-2009 23:41:48 | 只看该作者
原帖由 四香油饼 于 18-5-2009 12:23 发表



我们搞单片机的天天都得这么干啊

牛~~~~~~~~~~~~~~~~~~~~~~~~~~
回复  

举报

9#
发表于 19-5-2009 11:33:36 | 只看该作者

回复 #6 四香油饼 的帖子

我一朋友搞单片机,从来都是直接写汇编,搞ARM7、ARM9之类的,好多偷懒的就直接用C了
回复  

举报

10#
发表于 19-5-2009 15:58:56 | 只看该作者
我也是搞单片机的,个人感觉,如果有支持的较好的c编译器,还是用c写大部分程序,关键部分嵌入汇编或者汇编模块比较好。因为现在ram,rom的价钱已经不太昂贵,稍大一点对成本影响不大,而目前开发周期要求越来越重要了,而且好一点的c编译器出来的代码质量与一般普通的汇编程序员的代码质量相比,相差不多,甚至可能更好。但c语言的可读性,可维护性,模块化等等要好很多。
对于arm这样的,我想没人会用汇编完成整个系统的。近乎不可能完成的任务。呵呵

评分

参与人数 1威望 +30 收起 理由
coredump + 30 我很赞同!

查看全部评分

回复  

举报

11#
发表于 19-5-2009 16:09:08 | 只看该作者

回复 #10 dack 的帖子

老兄在哪里?在墨尔本不?交流交流
回复  

举报

12#
发表于 19-5-2009 17:49:05 | 只看该作者
在墨尔本,chadstone。
回复  

举报

13#
发表于 19-5-2009 19:10:21 | 只看该作者

回复 #10 dack 的帖子

现在很多的所谓嵌入式环境的计算能力都早就大大超过UNIX诞生时的PDP-11了,只是还用在嵌入式的环境中罢了,的确绝大部分已经不再需要从汇编中榨取那点性能了。
回复  

举报

14#
发表于 19-5-2009 19:19:06 | 只看该作者
原帖由 dack 于 19-5-2009 15:58 发表
我也是搞单片机的,个人感觉,如果有支持的较好的c编译器,还是用c写大部分程序,关键部分嵌入汇编或者汇编模块比较好。因为现在ram,rom的价钱已经不太昂贵,稍大一点对成本影响不大,而目前开发周期要求越来越重要 ...


您有所不知啊,我那位朋友偏爱AVR,大部分AVR MCU的RAM也就128/256/512 Byte,一个C的数组变量申明就都吃光了
回复  

举报

15#
发表于 19-5-2009 19:35:03 | 只看该作者
原帖由 ubuntuhk 于 19-5-2009 19:19 发表


您有所不知啊,我那位朋友偏爱AVR,大部分AVR MCU的RAM也就128/256/512 Byte,一个C的数组变量申明就都吃光了

巧啊,我对avr也有点了解,怎么说呢,avr的c编译器还是比较好的,用c写ram,rom占用并不会大很多,而且avr并不便宜,通常不会用在很低成本的低档机器里。我用atmega32写了一个充电器做练习,其中的所有模块都有用到,还用了avrx做操作系统管理任务。大概有十几个任务。
回复  

举报

16#
发表于 20-5-2009 00:57:15 | 只看该作者

回复 #15 dack 的帖子

佩服一下,还上了一个OS来管理AVR上运行的十几个任务

AVR这种C编译器编译出来的ASM代码再进行优化还是可行的,不过ARM7或ARM9(特别是ARM9),我也赞同应该主力用C,只对局部的算法、函数进行优化就足够了。
回复  

举报

17#
发表于 20-5-2009 09:28:04 | 只看该作者
原帖由 dack 于 19-5-2009 17:49 发表
在墨尔本,chadstone。


怎么搞单片机的都住chadstone阿?俺原来也住那
回复  

举报

18#
发表于 20-5-2009 09:29:56 | 只看该作者
avr 不错了。俺一般都是逢年过节才用用的。
回复  

举报

19#
发表于 21-5-2009 13:13:54 | 只看该作者

回复 #18 四香油饼 的帖子

这么惨啊,安慰一下
回复  

举报

您需要登录后才可以回帖 登录 | FreeOZ用户注册

本版积分规则

小黑屋|手机版|Archiver|FreeOZ论坛

GMT+10, 18-4-2025 15:02 , Processed in 0.038925 second(s), 38 queries , Gzip On, Redis On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表