双机调试内核调试器设置失败
没有 计划代码缺陷; 它们是由于无法理解不同输入条件下的执行流程而导致的事故 。 但是就像安全气囊是汽车的最后一道防线一样, 调试器也应该是程序员的最后一道防线。 防御性驾驶减少或消除了对安全气囊的依赖。 防御性驾驶是积极主动的 ,它利用所有可用数据来预测和避免问题。
防御性驾驶另一种方法是你是在一个驱动漫不经心和享受驾驶
拆除德比。 存在硬件和软件调试器必不可少且无可替代的情况。 但是大多数时候,调试器并不是发现和消除缺陷的最佳工具。
调试器不会为您修复该缺陷。
调试器与洗衣机不同 ,您可以在其中放入代码和肥皂,去喝咖啡和看电视,然后在30分钟后清除缺陷 。 只有了解了问题的根本原因后,才能修复缺陷。 请记住, 消除缺陷不会推进您的项目; 您的项目将暂停,直到解决问题。 确保缺陷永远不会进入您的代码,而不是学会变得有效地消除它们,这将为您提供更好的服务。 最糟糕的程序员在调试器中花费的时间是最好的程序员的25倍。 ( 链接 )
灵感而非汗水
缺陷是对程序行为的观察 ,它偏离了特定要求(无要求=无缺陷)。 熟悉代码路径的开发人员可以通过简单地推理观察结果的方式来发现缺陷。
在McGill ,我是必须进行大型机编程的最后一年。 大型机上不存在调试器,您只需将代码提交以在批处理系统中执行,如果有缺陷,则进行代码转储,然后将清单和转储发送给打印机。 在大学大型机的本质是,你会在与其他10-15人们都在等待他们的打印输出的高速打印机排队 。
我无事可做,我会考虑程序如何失败 –在大多数情况下,我可以在列印清单之前弄清楚缺陷的来源。 吸取的教训是思考的能力对发现缺陷至关重要。 无法替代通过缺陷进行推理的能力。 但是,如果您尽可能简单地放置代码路径,则将加快发现缺陷的过程。 此外,如果有复杂的代码段,则可以通过重写它们而受益,特别是如果它们是由其他人编写的。
当你想要的时候不在那里
即使您认为调试器是最好的发明,因为切片面包,我们大多数人都不能在客户的生产 环境中使用调试器。
在具有最少O / S基础架构的计算机上,有如此多的生产环境被防火墙所封闭。 大多数客户不会让您混乱他们的生产环境,因此在这些计算机上获取调试器的符号信息将是天作之举 。 在那些环境中,典型的解决方案是使用良好的日志记录系统 (即Log4J等)。 日志记录调用的放置位置与引发异常的位置相同 –但是由于这是调试器的讨论而非调试,我们将在另一时间讨论日志记录。
调试器和幻影问题
我为一个客户端工作,在那里启用编译器调试信息实际上导致间歇性错误弹出。 该程序将随机崩溃,没有明显的原因,我们都对问题的根源感到困惑。 有趣的是,问题仅发生在我们的开发环境中,而不是生产环境中。 我们的开发性能相当缓慢 ,我怀疑带有符号信息的代码文件可能是问题的根源。 我开始在没有符号信息的情况下编译程序,以查看是否可以提高性能。 令我惊讶的是 ,我们不仅获得了性能提升,而且随机崩溃也消失了。 通过进一步调查,我们发现调试器的符号信息占用了太多的RAM,从而导致内存溢出 。
调试器和生产力
对开发人员生产力的 研究表明,生产力最高的开发人员至少比其他所有人快10倍 。 如果您与一个生产效率高的人一起工作,然后问他们在调试器中花费了多少时间,很可能他们没有……
你知道查尔斯·普罗特斯·斯坦梅茨是谁吗? 他是电气工程的先驱,于1902年从GE退休。退休后,GE打电话给他,是因为他建造的非常复杂的系统之一被破坏了,GE的最佳技术人员无法修复它。 查理回来当顾问。 他在系统上走了几分钟,然后拿出粉笔,在零件上打了个X (他正确地识别出故障零件)。 查理随后向通用电气提交了10,000美元的帐单,但通用电气却被帐单的大小吓了一跳(今天大约30万美元)。 GE要求提供一份详细的咨询发票,Charles寄回了以下发票:
使粉笔标记1美元
知道在哪里放置它$ 9999
调试员分神
正如Brian W. Kernighan和Rob Pike所说的那样,他们在其出色的著作《 编程实践 》中
作为个人选择,除了获取堆栈跟踪或一两个变量的值之外,我们倾向于不使用调试器。 原因之一是很容易在复杂的数据结构和控制流的细节上迷失方向。 我们发现,单步执行程序的效率不如认真思考,并在关键位置添加输出语句和自检代码。 单击结束语句比扫描明智放置的显示的时间更长。 即使单步进入代码的关键部分,决定打印语句的位置所花的时间也更少,即使假设我们知道该位置。 更重要的是,调试语句保留在程序中。 调试会话是暂时的。 调试器就像希腊神话中的警报器一样,很容易被调试器显示的所有数据迷住并分散注意力 。
尽管现代调试器的显示在执行的点上下文数量惊人的能力,它只是代码的一个很小的部分的快照 。 这就像试图获得一幅绘画的图像,而您一次只能看到一个小窗口。 上面我们显示了一张图片随时间变化的6张快照,很难看到大图片。
上面的快照是蒙娜丽莎上的小窗户,但是很难从较小的快照中看到更大的画面 。 为了有效地调试问题,您需要对图片有一个更高层次的了解,以评估正在发生的事情(有效性)。 如果您必须在未编写的代码中发现缺陷,那么与仅在调试器中单步调试代码以期感到幸运一样 , 分析代码的总体结构(模块,对象等)仍然会更好。
当我们没有考虑相对于变量的所有可能数据组合,将通过代码采取的所有行为途径时,就会发生缺陷。
一些常见的缺陷类别包括:
- 未初始化的数据或缺少资源(即找不到文件)
- 代码路径设计不当
- 计算误差
- 持续性数据调整不当(即丢失数据)
关键是缺陷的行为实际上只有几种。 如果您了解这些常见行为,则可以构建代码以最大程度地减少发生这些行为的机会。
防御性编程
防御性编程的概念与我们进行防御性驾驶的方式相同,即“最好的防御是好的进攻”。 我的防御性编程的定义是使用技术来构建代码,以便途径很简单 , 易于理解。 尽力编写无缺陷的代码,而不是调试有缺陷的代码。 另外,如果在出现问题时我可以使程序知道其上下文,则它应该停止 并 提示错误消息,以便我确切地知道发生了什么。 这里有太多防御性编程技术无法涵盖所有这些,因此让我们仅举几个我喜欢的(向我发送您最喜欢的技术电子邮件 ):
- 正确使用例外
- 决策表
- 合同设计
- 自动化测试
正确使用例外
以下是设计代码途径的一种糟糕方法://程序不应到达此处。 任何写这样评论的开发人员都应该被枪杀 。毕竟,如果您错了并且程序真的到达那里该怎么办? 大多数程序中都存在这种情况,尤其是对于case / switch语句的默认情况。 您的评论不会停止该程序,它可能会继续在其他地方造成缺陷或副作用 。 您可能需要花费很长时间才能弄清楚最终缺陷的根本原因。 尤其是因为您可能会被自己的注释所吸引,而不考虑代码到达该点的可能性。 当然,您至少应该记录该事件,但是本文是关于调试器而不是调试的-因此,让我们将有关日志记录的讨论留到本文结尾。 至少您应该用以下内容替换注释:throw new Exception(“程序不应到达这里!”); 假设您有权访问异常 。 但是,这还远远不够。 毕竟,您掌握了到达该位置所必须满足的条件的信息。 该异常应反映这些条件,以便您立即了解看到的情况。 例如,如果到达代码中该位置的唯一方法是因为Customer对象不一致,那么以下方法会更好:throw new InconsistentCustomerException (“新事件日期先于Customer的创建日期” + Customer.getIdentity ()); 如果曾经抛出InconsistentCustomerException,那么您一看到该缺陷,便可能拥有足够的信息来修复缺陷。
决策表
代码路径的不正确设计还有其他的体现,例如没有在复杂的逻辑中规划足够的代码路径。 即,您使用7条代码路径构建代码,但实际上需要8条路径来处理所有输入。 任何需要缺少 第 8条路径的数据都将变成计算错误或不正确的持久数据调整,并在代码中的其他地方引起问题。
当您的代码具有多个途径的复杂部分时,请创建一个决策表来帮助您计划代码。 决策表自1960年代就已经存在,它们是基于复杂决策主动设计代码段的最佳方法之一。 但是,除非计划将其更新,否则不要在决策中包括决策表(请参阅针对失败者的注释 )。
合同设计
合同设计 (DbC)的概念是1986年由Eiffel编程语言引入的。 我意识到很少有人会在Eiffel中编程,但是这个概念可以应用于任何语言。 基本思想是每个类函数都应具有在每次执行时都经过测试的前置条件和后置条件 。
在功能入口处检查前置条件,在功能出口处检查后置条件。 如果违反任何条件,则将引发异常 。
DbC具有侵入性 ,会增加程序中每个调用的开销 。 让我们看看有什么可以使这些开销值得的。 当我们编写程序时,调用栈可能会很深。
假设我们具有以下调用结构; 如您所见,可以通过不同的呼叫路径(即,通过任何呼叫F或G的人 )到达H。 假设来自A的呼叫流有一个缺陷,需要在H中修复,很明显,这将影响来自B的呼叫流。 如果对H的修复将导致F中出现副作用,则很可能F (或B )的DbC后置条件将抓住问题并引发异常。 将其与解决没有制衡的问题相对照。 副作用可能表明与创建问题的距离很远,并且会导致很难跟踪的间歇性问题。
通过面向方面的编程的DbC
显然,我们中很少有人在埃菲尔编程。 如果可以访问面向方面的编程 (AOP),则可以通过AOP实现DbC。 今天,已经有AOP实现作为语言扩展或作为许多当前语言(Java,.NET,C ++,PHP,Perl,Python,Ruby等)的库。
自动化测试
正常的应用程序使用将在日常应用程序使用中行使很少的代码途径。 对于每个正常的代码途径,将有几种替代途径来处理异常处理,而这正是发现大多数缺陷的地方。 这些异常中的一些不经常发生,因为它涉及的输入数据不够完美–您是否真的希望那些异常在故障排除工具很少的远程客户系统上首次发生? 如果您使用的是测试驱动开发 (TDD),那么您已经对所有代码进行了一系列单元测试 。 这个想法很好,但是我认为还不够。 自动化测试不仅应执行单元测试,还应在集成级别执行测试。 这不仅会在类级别以异常条件测试您的代码,而且还将在类和模块之间的集成级别测试代码。 最终,应该使用规范数据库来增强这些测试,在规范数据库中还可以测试数据副作用。
这个想法是建立您的自动化测试,直到代码覆盖率达到80%以上 。 达到80%以上意味着您对系统途径的了解会非常好,并且修复缺陷所需的时间应该大大减少。 为什么超过80%? (请参阅最小可接受代码覆盖率 )
如果您可以达到80%以上的水平,这也意味着您对代码路径有很好的了解 。 代码覆盖率测试的优点是它不会影响代码的运行时性能,并且可以增进您对代码的理解。 您所有的缺陷都隐藏在未经测试的代码中。
减少对调试器的依赖
如果您必须求助于调试器来发现缺陷,那么请在解决问题后等待几分钟,然后问:“ 是否有更快的方法? ”。 您从使用调试器中学到了什么? 您是否可以采取一些措施来防止您(或其他人)再次沿着同一路径使用调试器? 重构代码或放入更好的异常会有帮助吗? 解决缺陷的唯一方法是通过了解代码中正在发生的事情。 因此,下次您有一个缺陷需要解决时,请先考虑问题。 花几分钟时间,看看是否无法解决问题,然后再加载调试器。
另一种技术是与其他开发人员讨论缺陷的来源。 即使他们不熟悉您的代码,他们也会提示您一些问题,这些问题通常会帮助您找到缺陷的源头而无需借助调试器。
在几家公司中,我说服开发人员放弃使用调试器一个月。 实际上,我们在每个人的系统上都删除了调试器的代码。 这种激烈的行为迫使开发人员考虑缺陷的产生原因。 开发人员抱怨了大约2周,但在一个月内, 他们发现缺陷的平均时间下降了50%以上 。 尝试在没有调试器的情况下工作一个星期,您会失去什么?
毕竟,下一张图显示了我们为消除缺陷花费了多少精力,平均每个项目最终将花费总时间的30%专门用于缺陷消除。 想象一下,将效率提高50%对您的项目有什么影响? 黄色区域勾勒出大多数项目落在哪里。
功能点 | 平均努力 |
10 | 1个人月 |
100 | 7个人月 |
1000 | 84人月 |
10000 | 225 人年 |
结论
肯定会有很多时候, 调试器将是发现问题的最佳解决方案。 当您花时间在调试器中时,您的项目处于暂停状态 ,它将无法进行。 一些程序具有复杂且复杂的部分,从而产生许多缺陷。 而不是使用调试器遍历这些部分,您应该对代码进行更高级别的分析并重写有问题的部分。 如果您发现自己在调试器中多次经历相同的代码路径,那么您可能想停下来问自己:“ 有没有更好的方法? 就像Master Card可能会说的那样:“ 不使用调试器……无价 ”。
没错,我是所有人中最大的“失败者”。 我相信我在书中至少犯了一次错误
翻译自: https://www.javacodegeeks.com/2013/05/debuggers-are-for-losers.html
双机调试内核调试器设置失败