自动patch推荐:从良好调试习惯到科学哲学
解bug可能是开发人员最经常做也最烦的一件事情,能够自动解bug一直是程序员的梦想。随着深度学习技术红利的爆发,基于大数据统计机器学习来自动解bug的期盼就更加水涨船高了。
别象小兄弟的PRECFIX最近更是在这个本来就热门的方向上又浇了一瓢油。身边的小兄弟们还有测试的兄弟们也对这个方向很着迷。各种机缘巧合之下,我也被卷到了这个方向上来。
调试之殇
这当然是一个充满了希望的田野。要想做到自动patch推荐,首先是要发现和定位问题。经过调研,我惊讶地发现,在软件开发技术日新月益、各种测试手段日臻完善、CI/CD等集成技术跨越式发展、组织和管理形式不断创新的今天,占软件开发50%以上时间的软件调试方向还处于刀耕火种的时代。
调试工具和集成开发环境其实发展很快,编译器的提示信息越来越精准和友好,各种静态扫描工具也不断更新换代,编程语言的生产力也在不断提升,各种日志分析工具也越来越强大,按理说定位问题应该更容易了,但是从统计数据上没有发现。
举个例子来说,别象说“虽然大多数线上代码都已经达到了很高的测试覆盖率,但是由于代码质量参差,有一大部分代码库是缺乏测试用例的,我们也缺少足够的缺陷报告去直接定位缺陷。”
在1993年就出版的《Code Complete》中的调试一章中,科学的调试方法The scientific method of debugging中,第一条就是Stabilize the error,把错误的发生稳定下来,而要通过可重复的试验收集数据的话,编写使错误可重现的单元测试脚本是第一步。
几十年前Iris Vessey的话“程序员往往不愿意使用现成的数据来约束他们的推理。他们往往喜欢进行琐碎和无理性的修改,而且他们通常不愿意推翻以前不正确的修改”,看起来现在仍然如此。
为什么会如此呢?因为技术虽然进步了,但是人性没有变。《代码大全》中列出的几件事情,现在仍然每天在上演:
- “我们每个人的投机心理都宁愿去用一种有可能在五分钟内发现缺陷的高风险方法,也不愿意为某种保证能找出缺陷的方法花上半个小时”
- “压力-通常是来自自身-将会使程序员更倾向于采用随机测试查找错误,并且让程序员在没经过验证的情况下武断判定这样方法能够奏效”
而正确的方法是什么呢?
- “通过那些能重现和不能重现错误的测试用例,对缺隐进行三角定位。直到你能真正地理解问题,每次都能正确地预测出运行结果为止”。
- “花点时间运行测试用例,证明你的假设,并证伪你的假设的逆命题”
- “增加能暴露问题的单元测试”
对于人类良好的调试习惯,给自动化处理也带来了良好的契机。2005年左右,利用发现问题的测试脚本覆盖情况的频谱分析定位技术(以下简称SBFL)就开始流行了。
一直到上个星期出的论文《Relationship between the Effectiveness of
Spectrum-Based Fault Localization and Bug-Fix Types in JavaScript Programs》,仍然在用这种古老而有效的技术。
自动patch推荐,本质上是一种自动化调试技术。给开发者的建议,如“利用否定性测试用例的结果”,“对之前出现过缺陷的类和子程序保持警惕”,“增量式集成”之类的,几乎都可以对应到自动缺陷定位和自动修复技术之中。
所以,只要按照教科书上的要求老老实实做调试,而不是随机撞运气,就可以给自动化修复带来算法上不可能推荐出来的结果。这也是这条路充满希望的重要原因。
我们来看作为学术研究标准的Defects4j数据集,人家是提供了详尽的测试用例的。
大家不妨做一个思想实验,如果我们解的bug质量不如标准数据集高,那么推荐出来的效果会不会比标准数据集的更好?
与最聪明的大脑对话
对于SBFL等技术,因为实在太踏实了,被诟病泛化性不好。其实并不是的,能够重复性的定位到问题,针对硬件的复杂性,环境的复杂性等等不确定性因素,已经是很了不起的一件事情了。张银奎大牛的《软件调试》第一版超过一千页。虽然软件调试不像软件工程一样成为一门核心课程,但是绝不说明它的重要性和复杂性低。写代码本身就已经是非常复杂的工作了,但起码还有形式化方法之类的
如果真要好的泛化性能不能实现呢?我们看来一个例子,目前最好的静态扫描工具之一的facebook的infer。
infer的基本逻辑是separation logic和bi-abduction。
丝毫不意外的,追求泛化不可能通过不完全归纲法写一堆if-then,需要抽象成一些数理逻辑的推导。infer使用了对霍尔逻辑的一个扩展,定义了堆上操作的一些小公理进行局部推理。
为了精确一些,我就引用原文了“In general, bi-abduction provides a way to infer a pre/post specs from bare code, as long as we know specs for the primitives at the base level of the code. The human does not need to write preconditions and postconditions for all the procedures, which is the key to having a high level of automation.”
通过对代码进行前置和后置的推断,不用人工写代码,可以自动生成前置和后置条件的判断,从而推断出可能的资源泄漏。
那么这种想法是怎么来的呢?
“Abductive inference was introduced by the philosopher Charles Peirce, and described as the mechanism underpinning hypothesis formation (or, guessing what might be true about the world), the most creative part of the scientific process.”
用人话来说就是哲学家逻辑学家查尔斯-皮尔士用于猜想世界的真相所引入的方法。
皮尔士为我们所熟知的是因为他是实用主义的创始人。其实他自己认为他更主要是个逻辑学家。他的数据逻辑方面的很多贡献其实现在是归入到知识论和科学哲学之中的。
光说数理逻辑这一块,就有弗雷格、康托、皮亚诺、希尔伯特、策梅洛、哥德尔、塔尔斯基等闪亮的名字。
写代码的过程,还可以依靠形式化方法之类的像演绎逻辑一样有其确定性,更容易自信一些。而分析bug的过程,寻找产生问题的原因,要靠归纳法,而归纳法早就被休谟一棒子打倒了,走向怀疑人生才是正常的。对于解决复杂问题的经验,上升到知识论或者科学哲学的高度其实并不为过。
我们如何做?
面对这么复杂的世界,我们还要实现自动化?我们如何做?
我觉得有三点:
- 去一线实践,学习一线的知识。比如千页的调试指南,硬件知识,环境相关的知识等等。就如同我们说的科学调试方法,不管怎么样都比瞎猜试错要强
- 利用数学,尤其是统计学。在实践中进行统计,应用各种机器学习方法。承上启下,一方面可以应用一线知识,一方面可以通过统计发现新知识,这部分也是技术红利所在
- 知识工程,将统计总结出的知识形成方法论。至少也得为统计出来的结果提供解释的假说。假如说patch推荐做成炼金术,我们不知道为什么行,反正就是行,这样的解释会不会被开发同学打死?
能写方法论的都是大神级的人物。《俞军的产品方法论》很火很受启发。史上叫方法论的更是牛到不行,比如笛卡尔的《方法论》,谢林的《学术研究方法论》,马克斯-韦伯的《社会科学方法论》。
在方法上的创新经常带来新的局面,笛卡尔提出了思辨方法,于是成为近代哲学之父,康德有先验方法所以是哥白尼式的革命,胡塞尔提出了现象学所以成为了现代哲学之父。
扯得有点远了,但是我想说的就是水真的挺深的,工作量也很大。从软硬件调试的知识,到数理逻辑,统计学和机器学习,再到一些偏哲学的知识,烧脑没商量。
但是,这样的烧脑是值得的。作为一个开发者,懂得调试是吃饭的本钱;作为一个现代人,懂数理逻辑和统计学是我们认识这个复杂世界的必由之路。总之,不赔。
道就讲这么多,从下一节开始我们讲术,从调试方法到数理逻辑到机器学习的各种实际的方法。