《编程匠艺》之开发工具与技巧
第二部分: 代码的神秘生命(代码开发的技术与技巧)
1. 欲善其事,先利其器(使用工具)
- 尽可能全面的了解你的常用工具, 为此投入的时间是值得的.
- 使用工具发挥作用:
- 了解它能做什么
- 学习如何驾驭它
- 了解它适合什么任务
- 检查它是否可用
- 找到了解更多信息的途径
- 工具链的组成
- 源代码编辑工具
- 代码编辑器
- 代码处理工具(diff比较工具, sed流查找修改工具, awk样式匹配工具, grep正则匹配工具, find/locate文件查找工具)
- 代码浏览工具
- 版本控制工具
- 源代码生成工具
- 源代码美化工具
- 代码构建工具
- 编译器(优化到什么程度?编译器遵循的标准是什么?如何生成正确指令集的代码)
- 链接器(是否可以生成库对象, 静态库,动态库;是否能生成可调试文件)
- 构建环境(make, automake, cmake等)
- 测试工具链(如自动化单元测试框架, 模拟内存不足, 负载过高等)
- 调试和调查工具
- 调试器(gdb, ddd)
- 分析器(性能分析等)
- 代码校验器(静态检查如lint, 动态工具如内存分配/边界检查器)
- 度量工具(统计函数,注/代 比, 圈复杂度, 重复度等)
- 反汇编工具
- 缺陷跟踪系统
- 语言支持工具
- 编程语言本身(语言特性,如go协程)
- 运行时和解释程序(如java虚拟机)
- 组件和库()
- 其他工具
- 文档工具
- 项目管理
- 源代码编辑工具
- 工具总结
- 你的工具箱中有哪些工具?哪些是频繁使用的?
- 是否已经发挥了工具的最大作用?是否尝试优化使用?
- 是否已经是能找到的最好工具?
2. 测试代码的魔术(保证代码质量)
-
测试分为不同的层次,作为开发人员,主要关注的是在代码实现过程中的测试.
-
关于测试的疑问:
- 为什么需要测试?这个应该不需要再复述了,为了保证交付的软件是满足要求的.
- 谁来测试?自己应该为自己写的代码进行充分的测试,而不是寄希望与测试人员.
- 测试内容都有哪些?这是个很好的问题,需要测试代码中的函数,类,流程等,根据测试的内容不同,可以分为单元测试和功能测试与集成测试.
- 何时开始测试?越早开始成本越低.
-
影响测试难度的因素:
- 分支与条件多少
- 代码规模
- 依赖关系
- 外部输入
- 多线程
- 代码演变
-
测试类型
- 单元测试(测试类或者函数)
- 组件测试(验证一个或者多个单元组成的完整组件行为)
- 集成测试(测试下一个系统中的多个组件,确保正确相连)
- 回归测试
- 负载测试(确保性能达到)
- 压力测试(确保超负荷时,不会乱成一团,用于获取软件的实际容量)
- 疲劳测试(使代码在较高负荷下连续工作一段时间,以确定是否有内存泄露和内存碎片化造成的性能降低)
- 可用性测试(用户使用测试)
-
挑选单元测试用例
- 既然不能测试所有的情况,那么怎么挑选用例呢?可以挑选下面的主要点:
- 良好的输入
- 不好的输入(如极大值,极小值, 过长或者过短字符串, 空字符串)
- 边界值(边界值本身,边界值上方, 边界值下方)
- 随机数(自动化随机产生)
- 零值
- 既然不能测试所有的情况,那么怎么挑选用例呢?可以挑选下面的主要点:
-
编写可测试的代码
- 使各部分代码自包含,尽量减少不必要的关联
- 不要依赖于全局变量
- 限制代码的复杂度,拆分代码.
- 保持代码的可观测性.
-
测试自动化
- 将自动化运行的单元测试成为你构建的一部分
- 测试的代码应尽量逻辑简单,避免测试代码出现问题
-
故障描述
- 在测试出现问题时,需要详细的描述问题,描述的内容包括:
- 出现问题时的环境(软件版本,硬件版本)
- 可以使问题复现的最简单的步骤
- 关于问题出现的可重复性和频率
- 有可能相关联的其他事物
- 在测试出现问题时,需要详细的描述问题,描述的内容包括:
-
在测试时出现的问题就应该进行跟踪,并对其进行自动化测试的覆盖.
-
开发测试管理
- 缺陷跟踪系统(报告故障, 分配责任, 确定优先级, 标记状态)
- bug审查(重点在于讨论缺陷以及如何处理, 不要讨论修改细节)
-
单元测试需要到什么程度?如果一段代码简单的看一下已经不能证明是否正确的时候,就该引入测试用例了.
-
测试驱动开发的模式, 编写代码之前的测试只能是黑盒测试.
3. 寻找缺陷, 并解决它(如何解决问题)
- bug的种类(如果你能准确知道它, 就能控制它)
- 从远处看(三类)
- 编译失败
- 运行时崩溃
- 非预期的输出行为
- 从近处看(更细致的分类)
- 句法错误(避免的方式是打开所有编译告警,并使代码通过lint检查)
- 构建错误(彻底清除中间构建, 从头构建)
- 语义错误(变量未初始化, 比较浮点数, 数值溢出, 隐士类型转换; 使用lint检查)
- 从更近处看(语义缺陷)
- 段错误(主要是错误的指针使用)
- 内存溢出(表现可能是运行很远处出现莫名的错误)
- 内存泄露()
- 内存耗尽
- 数学错误(浮点异常, 溢出, 除数为0等)
- 程序暂停(无限循环, 死锁, 竞争)
- 除错的艺术
- 地下之路
- 地上之路
- 调试工具
- 调试器(如gdb)
- 内存访问校验器(确认是否有内存泄露和溢出)
- 系统调用追踪(如strace)
- 内核转储core文件
- 日志
- 静态分析器
- 调试箴言
- 避免使用调试器"闲逛", 要注意调试黄金法则:多动脑子.
4. 代码构建
- 主要的构建机制有三种:
- 解释型语言
- 编译型语言
- 字节型语言
- 同一项目中,所有的成员都应该使用相同的构建系统,否则,构建的就不是同一个软件,可能存在参数等差异.
- 构建应该注意的事项:
- 构建完成后,需要为发行打包
- 每个版本都需要存档存储,或者在git上打一个版本tag.
- 每个版本都需要一个发行说明
- 当构建版本时,必须选择正确的编译器开关集.
5. 优化代码(追求速度和效率)
-
软件优化的含义:
- 程序的执行速度加快
- 减小可执行文件的大小
- 提高代码的质量
- 提高计算结果的准确性
- 将启动时间减到最小
- 增加数据的吞吐量
- 减少存储开销
-
造成代码臃肿,运行慢,体积大的原因?
- 不必要的复杂性
- 间接(额外的中间层)
- 重复(重复的调用复杂的计算过程)
- 糟糕的设计(加大了沟通的模块的距离)
- I/O等待
-
为什么不进行优化?
- balabala...备选方案
-
为什么要进行优化?
- balabala...特殊领域如游戏, dsp, 实时系统, 金融计算等领域.
-
怎么进行优化?
- 确定程度运行的慢, 并证明确实需要优化(但是要注意,也许并不是慢在代码级,而是设计上有问题)
- 找出运行最慢的代码,以这段代码为目标(使用合适的分析工具)
- 先测试这段的性能
- 对这段代码优化
- 测试优化后的代码是否功能正常
- 测试速度提升多少,并决定下一步
-
怎么分析哪段代码最慢?
- 使用合适的分析工具
- 手动添加计时
- 计算每个函数的调用频率(有工具, 也可以利用编译器的hook)
- 通过单个函数变慢来测试它对整个程序执行时间的影响
- 在进行分析时,要谨慎的选择分析数据,可以选择基本的数据集, 高负荷的数据集和普通的数据集.
-
优化的技术
- 优化有两种大的方向:修改设计和修改代码
- 基于运行速度的优化包括:
- 加快较慢代码的速度
- 尽量少做较慢的事情
- 将较慢的事情推迟到不得不进行的时候
- 代码设计层的修改包括:
- 添加缓存层, 加快较慢的数据的访问
- 使用资源池
- 为速度牺牲一点精度
- 变串行为并行,使用多线程模型
- 使用更合适的算法和数据结构
- 代码层的修改包括:
- 编译器的优化级别提高
- 循环展开
- 代码内嵌(inline)
- 移到编译时(如通过设置uint, 省掉<0 的检查)
- 强度折减(使用等价的操作替代其他指令, 如使用移位代替除法)
- 子表达式(对于多个地方会用到的同一个操作,抽出来)
- 无用代码删除
- (下面的方式更推荐)
- 如果发现一个函数慢, 那么不要频繁调用, 缓存结果
- 跨语言封装, 比如把java重新在c中实现
- 重新整理代码(推迟工作, 对函数做检查及时跳出, 循环条件中不做计算)
- 空间换时间, 提前缓存需要大量计算的结果
- 利用短路判断,把可能导致退出的条件放在前面
-
对程序性能产生深刻影响的决策有:
- 功能数量 VS 代码规模
- 程序速度 vs 内存消耗
- 存储和缓存 vs 按需计算
- 近似的计算 vs 精确的计算
- 内嵌 vs 函数调用; 单一的 vs 模块化的
- 通过引用或者地址传递 vs 传递副本
- 通过硬件实现 vs 通过软件
- 写死的直接访问 vs 间接访问
- 预先确定的固定的值 vs 可变可配置的值
- 编译时工作 vs 运行时工作
- 本地函数调用 vs 远程函数调用
- 巧妙的算法 vs 清晰的算法
-
较慢的程序的瓶颈可能在哪?
- 内存颠簸(不停的换出)
- 等待磁盘/网口等访问(等待I/O慢)
- 等待较慢的数据库事务
- 存在锁等待
-
对使用的语言和操作系统, 要大致了解其相关成本, 如函数调用, inline函数, 可以通过查看对应的指令, 来了解大致时间级别.
6. 安全的代码(防止被黑)
-
做安全防护的第一步是:了解你拥有哪些重要的资源, 是否拥有一些敏感的信息或者特定的能力.
-
不安全的软件源头:
- 不安全的设计和体系
- 缓冲区溢出
- 嵌入的查询字符串
- 竞争状况(常出现在复杂的多现场模型里)
- 整数溢出
-
编码中的保护方法
- 限制设计中的输入数量,安排所有的通信通过系统某部分进行
- 在尽可能低的权限上运行程序
- 避免开发并不真正需要的功能
- 不要依赖于不可靠的库
- 避免存储敏感数据
- 要对输入进行检查(包括命令行参数, 环境变量, web表单, 文件大小等);要检查输入的大小, 格式, 有效性以及数据的真正内容.