《编程匠艺》之开发工具与技巧

第二部分: 代码的神秘生命(代码开发的技术与技巧)

1. 欲善其事,先利其器(使用工具)

  1. 尽可能全面的了解你的常用工具, 为此投入的时间是值得的.
  2. 使用工具发挥作用:
    • 了解它能做什么
    • 学习如何驾驭它
    • 了解它适合什么任务
    • 检查它是否可用
    • 找到了解更多信息的途径
  3. 工具链的组成
    1. 源代码编辑工具
      • 代码编辑器
      • 代码处理工具(diff比较工具, sed流查找修改工具, awk样式匹配工具, grep正则匹配工具, find/locate文件查找工具)
      • 代码浏览工具
      • 版本控制工具
      • 源代码生成工具
      • 源代码美化工具
    2. 代码构建工具
      • 编译器(优化到什么程度?编译器遵循的标准是什么?如何生成正确指令集的代码)
      • 链接器(是否可以生成库对象, 静态库,动态库;是否能生成可调试文件)
      • 构建环境(make, automake, cmake等)
      • 测试工具链(如自动化单元测试框架, 模拟内存不足, 负载过高等)
    3. 调试和调查工具
      • 调试器(gdb, ddd)
      • 分析器(性能分析等)
      • 代码校验器(静态检查如lint, 动态工具如内存分配/边界检查器)
      • 度量工具(统计函数,注/代 比, 圈复杂度, 重复度等)
      • 反汇编工具
      • 缺陷跟踪系统
    4. 语言支持工具
      • 编程语言本身(语言特性,如go协程)
      • 运行时和解释程序(如java虚拟机)
      • 组件和库()
    5. 其他工具
      • 文档工具
      • 项目管理
  4. 工具总结
    • 你的工具箱中有哪些工具?哪些是频繁使用的?
    • 是否已经发挥了工具的最大作用?是否尝试优化使用?
    • 是否已经是能找到的最好工具?

2. 测试代码的魔术(保证代码质量)

  1. 测试分为不同的层次,作为开发人员,主要关注的是在代码实现过程中的测试.

  2. 关于测试的疑问:

    • 为什么需要测试?这个应该不需要再复述了,为了保证交付的软件是满足要求的.
    • 谁来测试?自己应该为自己写的代码进行充分的测试,而不是寄希望与测试人员.
    • 测试内容都有哪些?这是个很好的问题,需要测试代码中的函数,类,流程等,根据测试的内容不同,可以分为单元测试和功能测试与集成测试.
    • 何时开始测试?越早开始成本越低.
  3. 影响测试难度的因素:

    • 分支与条件多少
    • 代码规模
    • 依赖关系
    • 外部输入
    • 多线程
    • 代码演变
  4. 测试类型

    • 单元测试(测试类或者函数)
    • 组件测试(验证一个或者多个单元组成的完整组件行为)
    • 集成测试(测试下一个系统中的多个组件,确保正确相连)
    • 回归测试
    • 负载测试(确保性能达到)
    • 压力测试(确保超负荷时,不会乱成一团,用于获取软件的实际容量)
    • 疲劳测试(使代码在较高负荷下连续工作一段时间,以确定是否有内存泄露和内存碎片化造成的性能降低)
    • 可用性测试(用户使用测试)
  5. 挑选单元测试用例

    • 既然不能测试所有的情况,那么怎么挑选用例呢?可以挑选下面的主要点:
      1. 良好的输入
      2. 不好的输入(如极大值,极小值, 过长或者过短字符串, 空字符串)
      3. 边界值(边界值本身,边界值上方, 边界值下方)
      4. 随机数(自动化随机产生)
      5. 零值
  6. 编写可测试的代码

    • 使各部分代码自包含,尽量减少不必要的关联
    • 不要依赖于全局变量
    • 限制代码的复杂度,拆分代码.
    • 保持代码的可观测性.
  7. 测试自动化

    • 将自动化运行的单元测试成为你构建的一部分
    • 测试的代码应尽量逻辑简单,避免测试代码出现问题
  8. 故障描述

    • 在测试出现问题时,需要详细的描述问题,描述的内容包括:
      • 出现问题时的环境(软件版本,硬件版本)
      • 可以使问题复现的最简单的步骤
      • 关于问题出现的可重复性和频率
      • 有可能相关联的其他事物
  9. 在测试时出现的问题就应该进行跟踪,并对其进行自动化测试的覆盖.

  10. 开发测试管理

    • 缺陷跟踪系统(报告故障, 分配责任, 确定优先级, 标记状态)
    • bug审查(重点在于讨论缺陷以及如何处理, 不要讨论修改细节)
  11. 单元测试需要到什么程度?如果一段代码简单的看一下已经不能证明是否正确的时候,就该引入测试用例了.

  12. 测试驱动开发的模式, 编写代码之前的测试只能是黑盒测试.

3. 寻找缺陷, 并解决它(如何解决问题)

  1. bug的种类(如果你能准确知道它, 就能控制它)
    • 从远处看(三类)
    1. 编译失败
    2. 运行时崩溃
    3. 非预期的输出行为
    • 从近处看(更细致的分类)
    1. 句法错误(避免的方式是打开所有编译告警,并使代码通过lint检查)
    2. 构建错误(彻底清除中间构建, 从头构建)
    3. 语义错误(变量未初始化, 比较浮点数, 数值溢出, 隐士类型转换; 使用lint检查)
    • 从更近处看(语义缺陷)
    1. 段错误(主要是错误的指针使用)
    2. 内存溢出(表现可能是运行很远处出现莫名的错误)
    3. 内存泄露()
    4. 内存耗尽
    5. 数学错误(浮点异常, 溢出, 除数为0等)
    6. 程序暂停(无限循环, 死锁, 竞争)
  2. 除错的艺术
    • 地下之路
    • 地上之路
  3. 调试工具
    • 调试器(如gdb)
    • 内存访问校验器(确认是否有内存泄露和溢出)
    • 系统调用追踪(如strace)
    • 内核转储core文件
    • 日志
    • 静态分析器
  4. 调试箴言
    • 避免使用调试器"闲逛", 要注意调试黄金法则:多动脑子.

4. 代码构建

  1. 主要的构建机制有三种:
    • 解释型语言
    • 编译型语言
    • 字节型语言
  2. 同一项目中,所有的成员都应该使用相同的构建系统,否则,构建的就不是同一个软件,可能存在参数等差异.
  3. 构建应该注意的事项:
    • 构建完成后,需要为发行打包
    • 每个版本都需要存档存储,或者在git上打一个版本tag.
    • 每个版本都需要一个发行说明
    • 当构建版本时,必须选择正确的编译器开关集.

5. 优化代码(追求速度和效率)

  1. 软件优化的含义:

    • 程序的执行速度加快
    • 减小可执行文件的大小
    • 提高代码的质量
    • 提高计算结果的准确性
    • 将启动时间减到最小
    • 增加数据的吞吐量
    • 减少存储开销
  2. 造成代码臃肿,运行慢,体积大的原因?

    • 不必要的复杂性
    • 间接(额外的中间层)
    • 重复(重复的调用复杂的计算过程)
    • 糟糕的设计(加大了沟通的模块的距离)
    • I/O等待
  3. 为什么不进行优化?

    • balabala...备选方案
  4. 为什么要进行优化?

    • balabala...特殊领域如游戏, dsp, 实时系统, 金融计算等领域.
  5. 怎么进行优化?

    • 确定程度运行的慢, 并证明确实需要优化(但是要注意,也许并不是慢在代码级,而是设计上有问题)
    • 找出运行最慢的代码,以这段代码为目标(使用合适的分析工具)
    • 先测试这段的性能
    • 对这段代码优化
    • 测试优化后的代码是否功能正常
    • 测试速度提升多少,并决定下一步
  6. 怎么分析哪段代码最慢?

    • 使用合适的分析工具
    • 手动添加计时
    • 计算每个函数的调用频率(有工具, 也可以利用编译器的hook)
    • 通过单个函数变慢来测试它对整个程序执行时间的影响
    • 在进行分析时,要谨慎的选择分析数据,可以选择基本的数据集, 高负荷的数据集和普通的数据集.
  7. 优化的技术

    • 优化有两种大的方向:修改设计和修改代码
    • 基于运行速度的优化包括:
      • 加快较慢代码的速度
      • 尽量少做较慢的事情
      • 将较慢的事情推迟到不得不进行的时候
    • 代码设计层的修改包括:
      • 添加缓存层, 加快较慢的数据的访问
      • 使用资源池
      • 为速度牺牲一点精度
      • 变串行为并行,使用多线程模型
      • 使用更合适的算法和数据结构
    • 代码层的修改包括:
      • 编译器的优化级别提高
      • 循环展开
      • 代码内嵌(inline)
      • 移到编译时(如通过设置uint, 省掉<0 的检查)
      • 强度折减(使用等价的操作替代其他指令, 如使用移位代替除法)
      • 子表达式(对于多个地方会用到的同一个操作,抽出来)
      • 无用代码删除
      • (下面的方式更推荐)
      • 如果发现一个函数慢, 那么不要频繁调用, 缓存结果
      • 跨语言封装, 比如把java重新在c中实现
      • 重新整理代码(推迟工作, 对函数做检查及时跳出, 循环条件中不做计算)
      • 空间换时间, 提前缓存需要大量计算的结果
      • 利用短路判断,把可能导致退出的条件放在前面
  8. 对程序性能产生深刻影响的决策有:

    • 功能数量 VS 代码规模
    • 程序速度 vs 内存消耗
    • 存储和缓存 vs 按需计算
    • 近似的计算 vs 精确的计算
    • 内嵌 vs 函数调用; 单一的 vs 模块化的
    • 通过引用或者地址传递 vs 传递副本
    • 通过硬件实现 vs 通过软件
    • 写死的直接访问 vs 间接访问
    • 预先确定的固定的值 vs 可变可配置的值
    • 编译时工作 vs 运行时工作
    • 本地函数调用 vs 远程函数调用
    • 巧妙的算法 vs 清晰的算法
  9. 较慢的程序的瓶颈可能在哪?

    • 内存颠簸(不停的换出)
    • 等待磁盘/网口等访问(等待I/O慢)
    • 等待较慢的数据库事务
    • 存在锁等待
  10. 对使用的语言和操作系统, 要大致了解其相关成本, 如函数调用, inline函数, 可以通过查看对应的指令, 来了解大致时间级别.

6. 安全的代码(防止被黑)

  1. 做安全防护的第一步是:了解你拥有哪些重要的资源, 是否拥有一些敏感的信息或者特定的能力.

  2. 不安全的软件源头:

    • 不安全的设计和体系
    • 缓冲区溢出
    • 嵌入的查询字符串
    • 竞争状况(常出现在复杂的多现场模型里)
    • 整数溢出
  3. 编码中的保护方法

    • 限制设计中的输入数量,安排所有的通信通过系统某部分进行
    • 在尽可能低的权限上运行程序
    • 避免开发并不真正需要的功能
    • 不要依赖于不可靠的库
    • 避免存储敏感数据
    • 要对输入进行检查(包括命令行参数, 环境变量, web表单, 文件大小等);要检查输入的大小, 格式, 有效性以及数据的真正内容.
posted @ 2019-06-23 21:56  AISEED  阅读(264)  评论(0编辑  收藏  举报