大软件的烦恼与编译技术

转自 http://www.lingcc.com/2012/01/30/11974/

现在虽说卖硬件的比不上卖软件的,卖软件的比不上卖服务的。但软件仍然是服务的基石。
而且不管是软件、硬件、还是服务,背后都是一行行的代码,以及基于这些代码所形成的软件功能、硬件系统、技术人员的经验等等。
这些代码有的用C/C++、Java、C#写,有的用PHP、JavaScript、Ruby写,有的用Verilog写。

这些软件往往是公司的看家本领。比如Oracle的 Oracle数据库,Synopsis的EDA工具、Adobe的Photoshop、微软的Windows、Visual Studio等。
软件做的强了,难免要用到许许多多的平台上;用的人多了,又难免会有不停的修修补补。
对于靠编程手艺挣钱养家的各位朋友,相信绝大多数都调过遗产代码中的bug。
也大概都生过“代码太烂、文档太少、逻辑太混乱”的怨念。

年前曾和几位朋友交流了一下基于持续开发好多年的大软件(代码量30多G)上的开发、移植和debug问题。
这篇文章试图基于这次交流,结合自己的经验和认识,给出一些利用编译技术解决相关问题的方式。
虽然自己也是在大软件上开发,但因为尚未走出校园,所接触的开发也偏重小作坊的方式,了解和应用的手段还非常少,敬请各位不吝指正。

1 为什么会有这些问题?

一个大的软件项目,往往需要许多年的持续开发。虽然开始时,结构、逻辑、模块化都设计的非常好,号称各种情况都考虑到了。
项目文档写了一摞又一摞,但随着开发和测试的进行。都会有不少的bug冒出来,慢慢的缝缝补补越来越多。
原本设计的清清楚楚的逻辑就慢慢在源代码中不那么清晰了,代码的逻辑越来越复杂。

虽然一开始留下详细的设计和说明文档是完全可行的,但接下来的bug修补多数只能留下一些bug report。
要想把软件项目文档和 bug report整合在一起,形成持续更新、体系完整的详细文档,可不是那么容易的事情。
再加上我们在刚刚接触这类代码的时候,面对的是一个一无所知的世界,而看到的一行行代码许多都是针对细致末节的,计算机该如何如何一步步完成任务的详细说明,难懂和怨念也就不奇怪了。

1.1 代码层次上的问题

对于草根软件工程师,咱只能从代码上说了。
大软件意味着巨大的代码量,一般都是上千万行级的代码。
在这茫茫代码中,寻找你此刻想了解的某一段代码的定义、引用和与之相关的所有注释谈何容易?

幸好,有Source Insight、Ctags之类的工具,能让我们稍微找到一点点脉络。
不过当面对Linux、GCC这种跨平台软件时,这些工具也难免会找错函数、变量和类型的定义和引用位置。

另外,软件太大,编译时间也会很长,这也会降低开发速度。
虽然Makefile可以用-jN的方式,多进程同时编译。
不过大软件中,也不都能像GCC、Linux Kernel这样顺利使用-jN编译下来。
至少《编译点滴》博主在使用Gentoo系统的5年多时间里,出现过不少次make -j3有问题,但重新emerge一下就过的情况。
这种情况除了用依赖处理不好来解释外,我还没想到别的。

层层包含的头文件,也让问题的定位越来越难hold。
尤其是某些头文件中的定义又被一堆ifdef的宏扩在一起的时候。
乱七八糟的逻辑关系让人头疼。
虽然编译器可以提供预处理功能,适当的帮我们做些宏展开和完全不相干代码的删除工作。
但巨大的预处理文件,我们也只能从文件最后慢慢的往前找。

要是再来点刺激的并发支持,什么多线程、锁、共享变量。
靠!就能砸了显示器,跑去指着老板的鼻子说:什么破代码,爷不干了。

2 编译技术能作什么

其实,铺垫了这么多,就是一句话,编译技术其实能帮上忙。

编译器想必大家最熟悉的功能就是能将源代码转换成可执行文件。
不过编译器能做的不仅仅是这些。
对于常见的编译型命令式语言(C/C++),编译器理论上可以知道程序的所有行为。
之所以加”编译型、命令式“的定语是因为有些语言却是编译器没办法在编译时刻完全知道所有的行为。
比如 JavaScript语言可以通过eval(string)函数,在程序运行的时刻,重新组织一个Javascript语句的字符串,然后通过调用eval函数执行它。
这个字符串,有可能和输入相关。所以编译器无法在很早的时候知道它的行为。
之所以加上“理论上”,是因为某些分析工作状态空间太大,搜索复杂度太高,没办法在短时间内完成。
比如过程间分析、指针分析之类的代码分析技术,面对上百万、上千万的代码时,可不是一时半会儿能出结果的。

咱们不能只是说说问题有多难就算了,幸好我们已经有了不少的解决方法。
《编译点滴》了解了一些将编译技术应用于协助解决这些问题的领域。
用上编译技术的程序员,会男的更帅、女的更漂亮,早日找到另一半,还能调节心情、预防老年痴呆、避免2012人类灭亡。

2.1 精准的代码定位

这个可能是在阅读源代码时,最最实用的了。
但目前能做到对于大规模软件实用的,除了商业的source insight用于C/C++,其他的《编译点滴》还没怎么听说,欢迎各位朋友建议。

即便是强大的Source Insight,也还要看具体的代码情况。
比如对Open64这种包含不少ifdef宏的代码,即使是source insight,也要再做些针对性的设置才能hold住。
eclipse的CDT基本搞不定(本博主对Eclipse的了解有限,尝试了几次,都不好使,只好放弃)。
ctags也找的很乱,许多时候结果也不靠谱。

但这类代码分析工具就真的无能为力了吗?既然编译器都能编译得到可执行文件。可执行文件能找到正确的函数和变量定义。
那么代码分析工具就完全没有理由做不到这些。stackoverflow有篇文章(参考链接4),对比了若干常用代码阅读工具对待600M以上代码量时的情况.

现在纯粹利用编译器,产生tags信息提供给代码阅读软件使用的工具也有一些。比如GCC MELT、LLVM的libclang。
另外还有些代码库自带了tags生成机制,比如GCC编译时,执行“make tags”命令,就能生成emacs下可以使用的tags信息。
LLVM则一直都通过Doxygen,解析源码中的注释,自动生成代码文档。
这些tags信息的生成背后,都是编译前端的相关理论。

2.2 减小编译时间

代码基太大,编译的时间自然就会很长。修改一个头文件或者Makefile,想看看效果如何,就得重新编译几个小时,这个速度谁都受不了。
于是如何减小编译时间,就是个比较大的问题。

2.2.1 并行编译

比较常用的方式,是并行编译。如“make -jN”。这种方式在多核或者分布式环境中,效果最好。
这种机制最简单方便,因为编译器的输入都是一个个源程序文件,而这些源代码文件间的编译是相互独立的。
所以Makefile提供了并行编译机制,它会自动根据makefile中指定的依赖关系,启动多个编译器进程,同时编译。
一般N个核,启动N+1个进程同时编译效果比较好些。因为这样可以在充分利用多个核的前提下,保证不会有频繁的线程切换开销(参考链接10)。

分布式系统下,使用”make -jN“时,需要配合 distcc来完成。
distcc的基本原理是:预处理和链接阶段,因为和本地的头文件、库关系很大,所以必须在本地完成。
而预处理后文件的编译过程则可以爱在哪儿编,就在哪儿编。
只要编译器的版本相同,给它传递的选项相同,又使用同样的预处理输出文件,那么生成的.o文件一定是相同的。
参考链接11和12是有关的信息。

2.2.2 头文件搜索

我们都学过C语言中”#include“语句的作用是将被include的文件所有内容,在”#include“语句处完整的展开。
不过在大软件的研发中,模块化是很重要的,因此各个头文件的功能分割的很细,而且头文件A可能又include了B,B又include了C。
这样一个连环include之后,导致最后预处理后的文件非常大,而且实际被编译文件可能仅仅是用到了文件C中的某个类型的”typedef“定义而已。
如果这些头文件里定义了许多函数,那这些函数都将会被一一编译,况且头文件还会反复被许多源代码文件包含,于是就存在许多的重复编译。

预编译头文件就是常见的一种优化编译时间的方式(参考链接13,14,15)。Linux系统中,这类文件通常是原头文件名加上”.gch”的后缀。
GCC编译时,进入include文件搜索路径时,会先搜索.gch文件,找不到,再找原头文件。

不过这样还是存在函数重复编译的问题,其实一种最直观的想法是被编译的文件中需要什么,就从头文件中递归的将相关内容include进来。
这样就减小预处理后文件的大小,也就减小了编译时间。这方面有个典型的项目“Include-What-You-Use”(参考链接16)。
这个项目的5个发起人都来自Google,目前该项目处于活跃开发中。它利用clang来实现这一功能。

另外项目开发期,使用-O0编译,不做任何优化,也能提升点编译速度。

2.3 相关学术研究

Zhou Yuanyuan老师率领的研究小组在大软件和软件工程相关方向介绍了很多他们的工作。
并发表了很多高质量,并被工业界、学术界都认可的文章。
和传统的文章不同,周老师的文章基本都是从一个非常明确的实际问题出发。
所以这里《编译点滴》结合自己的知识和了解,介绍一些,并将所有的文章题目列在了最后。
zhou老师基本都会把他的文章直接放在自己的主页(http://cseweb.ucsd.edu/~yyzhou/)里,
所以文章直接Google应该就能搜索到,如果搜索不到的话,可以直接发邮件给我( lingcc@lingcc.com )索取.

除了zhou老师,肯定还有许多其他学者就大软件领域做了很多深入研究,限于《编译点滴》了解有限,无法一一列举。
若其他朋友有相关了解,欢迎提出。

2.3.1 注释检错和纠正

大规模软件通常都非常注重文档和代码的规范性。比如要有文档,要有注释,都要写的很详细等等。
不过在打过成千上百个patch之后,代码能保证正确执行就不错了,谁还管的了注释对不对。
这样久而久之,注释的准确性就很难保证的。
但是新手们在研究一段代码时,都愿意先看看注释,再看代码,那后果就可想而知了。

comment是给人看的,写的偏重自然语言;程序是写给计算机看的,都是形式化的语言。
如何将这两者结合起来,现在还停留在研究阶段。幸好,Lin Tan和Zhou Yuanyuan分享了他们在代码注释上的持续性的工作,比较有意思(参考文献17-21).
《编译点滴》仅仅阅读过参考文献17,主要介绍了他们将自然语言处理和编程语言的前端分析技术结合在一起,找出注释和代码的不符指出。

2.3.2 丰富的程序警告输出

当软件的规模很大时,既要从整体上把握整个软件的整个设计架构,又能在面对bug时能轻轻松松搞定可不是件容易的事情。
因此,为了能尽早的发现bug,很多软件在开发之初,就非常注意断言错的加入。
断言,通常是为了方便调试的考虑,在程序的代码中,预先判断是否满足某些应该满足的条件,若条件不满足时,就该想办法给开发者报告相关问题。

例如对编译器这种复杂的大型系统软件,如果没有丰富的断言机制,那调试起来将是噩梦。
因为你没办法确定在编译的稍前位置增加的某个程序变换,到底会不会带来问题。
如果只能靠最后生成的二进制文件的运行结果来判断编译器是否错误,对于编译器开发人员来说,只有两种结果:有薪水非常高的工作、或者这个世界上只剩下一个编译器。
因为调试太难了.
《编译点滴》博主在Open64上做过一些东西。丰富的断言为开发节约了很多调试时间。

像一些重要的应用程序,比如数据库、操作系统内核这种,相比在开发阶段,报告给程序员的断言。
还有一个比较重要的程序输出就是各种log。这些log也是开发人员用于发现和调试程序很得力的助手。

加个程序输出很容易,但怎么加,加些什么,加在哪里,却不是那么容易搞定的事情。
必要的断言,给程序的调试很有帮助。
但乱加一起,会带来很多的误报。
当然统一的设计这些警告的输出是很重要的。
对于已经基本成型的大型软件,如果在原代码的基础上改进呢?

Zhou Yuanyuan老师的团队最近发表的一篇文章研究了如何在已有大型软件中,让log输出的信息更丰富,方便debug。
想法其实不难,但能想到这一点,并踏踏实实的做出来,很难!(参考文献22-24).

《编译点滴》仅仅听zhou老师讲过他们SherLog的文章。
具体的做法,对于原来不带任何参数的单纯输出一行语句的操作,借助一定的程序分析,和条件判断。
将程序运行到log输出处参与完成程序分支判断的那些变量的值,和log一起输出出来。
这样log、断言错的信息就更加丰富,方便程序员重现和调试问题。
这样的工作,需要运用编译器前端的不少技术,再接和具体的问题。
很有意思。

2.3.3 Patch中的问题

周老师的这篇论文虽然没有用上编译技术,但是《编译点滴》觉得他们的发现很有意思,所以在这里咱们也聊一聊。

软件发布-》发现bug-》打补丁,这个过程几乎是所有发布的软件修复bug的主要方式。
但因为以下三个原因,新打的补丁反而更加的不可靠:

  • 此时bug的修复工作时间要求紧。工程师们不得不顶着很大的时间压力,尽快的修好bug,没有心思去作深入的思考。
    测试工程师的测试因为时间要求紧,也都是草草了事。这带来了很大的bug隐患。
  • 仅仅关注大软件系统中发现bug的那个部分。会花费很大的经历在bug本身,没有机会去考虑整个系统的正确性。
  • 因为大软件有很多的遗产代码,补丁的开发者、审阅者也很难弯曲理解代码和bug。

参考文献25研究了包括Linux、OpenSolaris、FreeBSD等操作系统内核的补丁情况。
所得结论如下:

  • 14.8%-24.4%的补丁是不对的
  • 并发的bug修复最困难,错误率最高。39%的并发bug的补丁是错的。
  • 通常错误的bug补丁来自于开发工程师和代码审阅人对代码本身的了解不够。

3 参考

  1. http://gcc-melt.org/
  2. http://clang-analyzer.llvm.org/
  3. http://stackoverflow.com/questions/5309405/can-i-get-an-xml-ast-dump-of-c-c-code-with-clang-without-using-the-compiler
  4. http://stackoverflow.com/questions/5842650/c-c-source-code-browser-comparison-seeking-opinion.
  5. http://clang.llvm.org/doxygen/group__CINDEX.html
  6. http://stackoverflow.com/questions/7969109/any-c-c-refactoring-tool-based-on-libclang-even-simplest-toy-example
  7. http://www.vim.org/scripts/script.php?script_id=3302
  8. http://eli.thegreenplace.net/2011/07/03/parsing-c-in-python-with-clang/
  9. http://msdn.microsoft.com/en-us/magazine/cc163658.aspx
  10. http://stackoverflow.com/questions/2499070/gnu-make-should-j-equal-number-the-number-of-cpu-cores-in-a-system
  11. http://code.google.com/p/distcc/
  12. http://www.gentoo.org/doc/en/distcc.xml
  13. http://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
  14. http://msdn.microsoft.com/en-us/library/szfdksca%28v=vs.71%29.aspx
  15. http://gamesfromwithin.com/the-care-and-feeding-of-pre-compiled-headers
  16. http://code.google.com/p/include-what-you-use/
  17. http://dl.acm.org/citation.cfm?doid=1985793.1985796
  18. *icomment: bugs or bad comments?* 2007
  19. HotComments: How to Make Program Comments More Useful? 2007
  20. Listening to programmers — Taxonomies and characteristics of comments in operating system code , 2009
  21. aComment: mining annotations from comments and code to detect interrupt related concurrency bugs, 2011
  22. Improving software diagnosability via log enhancement, ASPLOS 2011
  23. SherLog: error diagnosis by connecting clues from run-time logs. ASPLOS 2010
  24. Understanding Customer Problem Troubleshooting from Storage System Logs. FAST 2009
  25. How do fixes become bugs?. SIGSOFT FSE 2011
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值