程序性能优化

结合这两天碰到的一个实例简单聊一下这个话题。

本文带有很强的主观意识,若和各位看官的想法冲突,请持有保留态度阅读。


背景故事

这两天帮一个老朋友优化了一份代码,
大致功能是windows下枚举系统的一些关键信息,
规整并输出到指定格式的文件中。

选择合适的语言

在我的要求下,老朋友将代码发过来作为参考,cpp 实现,
大约有 5000 行代码,冗余严重,性能不容乐观。

这里出现了三个关键词,cpp冗余性能

CPP

cpp 被大部分人认为是接近 c 的底层语言。底层语言的性能通常较高,很多博客文章都会将其作为卖点而忽略了一个事实。

cpp 可以实现高性能程序,但不代表使用 cpp 实现的程序一定是高性能的。对应用该语言的人来说,通常使用 cpp 实现高性能、稳定的程序难度非常高。同时也很容易忽略一个问题,即程序本身是 计算密集型 还是 IO密集型,对于不同情景来说,cpp 未必是最适合的语言。

Linus 时隔十一年再次抨击 C++ :糟糕透顶!毫无用处!

就和 linus 多次吐槽 cpp 一样,由于其难度过高,很多开发者并不具备使用它的能力。一门语言在使用时应当是清晰的,透明的,其编码思路是编译器能理解并能提出相应建议的。cpp 显然在很多方面仍旧存在欠缺。

冗余

另一方面,使用 cpp 时容易重复造轮子,更可怕是容易在不熟悉系统框架时造出一些性能低下的轮子,并直接作用于项目。

轮子本身是好的,通过实践深入系统架构,加深理解的同时也能推陈出新。只是当项目目标不在轮子本身这块时,使用成熟的第三方框架是明智的选择。

性能

俗话说,“底层语言抠细节,高层语言搞设计”(刚编的),安卓系统就是一个好例子,使用 javacpp 分别去解决不同层面的问题。

前面提到了 计算密集型 还是 IO密集型。对于 计算密集型,当程序性能问题发生在大量复杂的运算中时,使用底层语言能减少抽象成本,生成出性能更好的指令。而对于 IO密集型 来说,程序自身的性能就不在首要位置了,大量的时间被消耗到了IO等待中。针对这一问题,系统层面又提出了多路复用等各种手段,简单的说,一是IO可用时去做IO,二是减少IO本身的开销,内存拷贝等,这些都是后话了。

小结

cpp 不是解决性能问题的万金油,应当从实际出发,选择更贴合目标的语言。

这也是老朋友为什么使用高性能语言却没能得到想要的高性能的主要原因之一。

从哪里开始优化

一般来说,不考虑重构的话,优化可以借助一些性能分析工具,通过对函数、方法的占用时间快速定位热点代码部分。

例如 pprofperf 工具生成信息,windows可以使用vs自带的性能分析工具。

无论使用哪种工具,需要重点关注的都是比较耗时的操作在哪个函数或者调用中。

优化的方向主要有:

  1. 函数结构、算法
  2. 框架或模型

无论是哪种,都应该带着“它为什么会耗时,它有没有优化的可能或必要性”这样的问题进行探索。

另一种便是在考虑重构时,对需求重新分析,尽可能的一开始就避免低效的代码。

编码前的思考

在这个实例中,首先进行需求分析。

  1. 枚举系统
  2. 规整数据
  3. 输出文件

朋友在编写代码时,认为能通过多线程并发解决数据量过大的问题,故在代码中创建了很多的线程。

多线程是个双刃剑。
它能有效利用CPU资源,这在编译器中很常见,多线程编译会快上许多,在解决 计算密集型 程序时,能有效发挥作用。但随之会引出一个问题,多线程中使用共享资源的同步问题。需要考虑到锁本身的开销就很大,线程资源的维护也存在开销。

线程并不是越多越好。可以考虑:

进程时间片会分给线程,若线程过多,那么每个线程的有效时间就会减少,线程切换本身就会消耗一定的计算资源,若线程的有效时间过低,切换线程的开销就会被凸显。

回到需求,枚举系统是一个 Native API 调用过程,整理数据是一个计算过程,输出文件是一个 IO 过程。

核心如下:

1
2
3
4
5
6
7
any foo() {
for (;;) {
any data = enum_system();
data.translate(FMT);
data.write2file();
}
}

简单分类,前两个过程都可以使用多线程优化,第三个则需要对文件IO优化。

仔细思考,如果将IO操作放到线程中处理会引发什么问题?

没错数据同步问题,会出现多个线程对同一个文件的写入操作。

此时再将IO操作放到独立的线程中,将成为一个典型的 PV问题。

有一个有限缓冲区和两个线程:生产者和消费者。他们分别不停地把产品放入缓冲区和从缓冲区中拿走产品。一个生产者在缓冲区满的时候必须等待,一个消费者在缓冲区空的时候也必须等待。另外,因为缓冲区是临界资源,所以生产者和消费者之间必须互斥执行。

关于PV问题的文章有很多,本文就不啰嗦了。

接着便是文件IO操作如何优化的问题。
通常情况下,使用异步IO即可,只是windows下实现起来异常复杂。
另一种简单的办法便是减少IO次数,引入写缓冲机制,即在写缓冲未满时,写入会保存到缓冲区,直到数据总量超过缓冲区大小时再一次写出。
这种办法对于频繁的小数据写出格外有效,符合上面的需求。

接下来还需要考虑乱序问题,即写出的数据乱序该如何处理。一旦引入这个问题,多线程就不见得是好事,若放到内存中排序,则需要大量的内存来存放这些数据。若使用锁,那多线程将失去原有的意义。当然也可以通过一些中间件进行处理。

杂谈

有位大师说过,要“避免过早的优化”,实时要求低的场景中,也许根本就不存在优化需求。

需求分析是很重要,对可接受的时间评估,如果能达到可接受范围,再考虑是否有迫切的优化需要。开始有一个预期,也能对后期优化指明方向,即对哪里可能存在的性能问题了然于胸。

对于某个底层调用非常耗时又不得不去使用时,在应用层面优化所产生的效益会大大降低。这种情景下,大佬们都会写底层的部分代码,针对某一部分特化处理。

总结

总之,性能问题中没有“银弹”,需要到不同纬度中寻找答案,不能盲目使用语言或多线程等手段绕开,也不是一味的去优化算法,需要实事求是,从性能问题的源头出发稳扎稳打,否则会适得其反。