年薪80W的架构师总结:性能优化其实不难,记住这十条策略就够了( 二 )


很多文件系统有预读的功能 , 就是提前从磁盘读取额外的数据 , 为下次上层应用程序读数据做准备 。这个功能对顺序读取非常有效 , 可以明显地减少磁盘请求的数量 , 从而提升读数据的性能 。
CPU 和内存也有相应的预取操作 , 就是将内存中的指令和数据 , 提前存放到缓存中 , 从而加快处理器执行速度 。缓存预取可以通过硬件或者软件实现 , 也就是分为硬件预取和软件预取两类 。
硬件预取是通过处理器中的硬件来实现的 。该硬件会一直监控正在执行程序中请求的指令或数据 , 并且根据既定规则 , 识别下一个程序需要的数据或指令并预取 。
软件预取是在程序编译的过程中 , 主动插入预取指令(prefetech) , 这个预取指令可以是编译器自己加的 , 也可以是我们加的代码 。这样在执行过程中 , 在指定位置就会进行预取的操作 。
4. 延后 / 惰性处理
延后 / 惰性处理策略和前面说的预先 / 提前处理正好相反 。就是尽量将操作(比如计算) , 推迟到必需执行的时刻 , 这样很可能避免多余的操作 , 甚至根本不用操作 。
运用这一策略最有名的例子 , 就是 COW(Copy On Write , 写时复制) 。假设多个线程都想操作一份数据 , 一般情况下 , 每个线程可以自己拷贝一份 , 放到自己的空间里面 。但是拷贝的操作很费时间 。系统如果采用惰性处理 , 就会将拷贝的操作推迟 。如果多个线程对这份数据只有读的请求 , 那么同一个数据资源是可以共享的 , 因为“读”的操作不会改变这份数据 。当某个线程需要修改这一数据时(写操作) , 系统就将资源拷贝一份给该线程使用 , 允许改写 , 这样就不会影响别的线程 。
COW 最广为人知的应用场景有两个 。一个是 Unix 系统 fork 调用产生的子进程共享父进程的地址空间 , 只有到某个子进程需要进行写操作才会拷贝一份 。另一个是高级语言的类和
容器 , 比如 JAVA 中的 CopyOnWrite 容器 , 用于多线程并发情况下的高效访问 。C++ 里面经常使用的 STL 标准模板库中的很多类 , 比如 string 类 , 也是具有写时才拷贝技术的类 。
三、并行 / 异步操作优化策略的第三大类是“并行 / 异步操作” 。并行和异步两种操作虽然看起来很不一样 , 其实有异曲同工之妙 , 就是都把一条流水线和处理过程分成了几条 , 不管是物理上分还是逻辑上分 。
5. 并行操作
并行操作是一种物理上把一条流水线分成好几条的策略 。直观上说 , 一个人干不完的活 , 那就多找几个人来干 。并行操作既增加了系统的吞吐量 , 又减少了用户的平均等待时间 。比如现代的 CPU 都有很多核 , 每个核上都可以独立地运行线程 , 这就是并行操作 。
并行操作需要我们的程序有扩展性 , 不能扩展的程序 , 就无法进行并行处理 。这里的并行概念有不同的粒度 , 比如是在服务器的粒度(所谓的横向扩展) , 还是在多线程的粒度 , 甚至是在指令级别的粒度 。
绝大多数互联网服务器 , 要么使用多进程 , 要么使用多线程来处理用户的请求 , 以充分利用多核 CPU 。另外一种情况就是在有 IO 阻塞的地方 , 也是非常适合使用多线程并行操作的 , 因为这种情况 CPU 基本上是空闲状态 , 多线程可以让 CPU 多干点活 。
6. 异步操作
异步操作这一策略和并行操作不同 , 这是一种逻辑上把一条流水线分成几条的策略 。
我们首先在编程的领域澄清一下概念:同步和异步 。同步和异步的区别在于一个函数调用之后 , 是否直接返回结果 。如果函数挂起 , 直到获得结果才返回 , 这是同步;如果函数马上返回 , 等数据到达再通知函数 , 那么这就是异步 。
我们知道 Unix 下的文件操作 , 是有 block 和 non-block 的方式的 , 有些系统调用也是block 式的 , 如:Socket 下的 select 等 。如果我们的程序一直是同步操作 , 那么就会非常影响性能 。采用异步操作的话 , 虽然稍微增加一点程序的复杂度 , 但会让性能的吞吐率有很大提升 。


推荐阅读