20.1 如何思考性能问题
要解决的第一个问题是“在正常的开发过程中,你应该对性能有多少担心?”如果你试图优化每条语句以获得最大的速度,就会减慢开发速度,并造成许多不必要的复杂性。此外,许多“优化”实际上对性能没有帮助。另一方面,如果你完全忽视性能问题,很容易导致大量明显的低效率问题分布在代码中;所产生的系统很容易比它需要的速度慢5-10倍。在这种“千刀万剐”的情况下,很难再回来改善性能,因为没有任何一项改进会产生很大的影响。
最好的方法是介于这两个极端之间,你使用基本的性能知识来选择 “自然高效”但又干净和简单的设计方案。关键是要认识到哪些操作从根本上来说是代价高昂的。下面是一些今天相对代价较高的操作的例子。
网络通信:即使在一个数据中心内,一次往返的信息交换也需要10-50微秒,也就是数万倍的指令时间。广域往返可能需要10-100毫秒。
到二级存储的I/O:磁盘I/O操作通常需要5-10毫秒,也就是数百万指令时间。闪存需要10-100微秒。新兴的非易失性存储器可能快到1微秒,但这仍然是大约2000个指令时间。
动态内存分配(C语言中的malloc,C++或Java中的new)通常涉及分配、释放和垃圾回收的大量开销。
缓存未命中:从DRAM中获取数据到片上处理器的缓存需要几百个指令时间;在许多程序中,总体性能由缓存未命中和计算成本决定。
了解哪些事情是昂贵的最好方法是运行微观基准测试(测量单个操作开销的小程序)。在RAMCloud项目中,我们创建了一个简单的程序,为微型基准测试提供了一个框架。创建该框架花了几天时间,但该框架使得在5到10分钟内增加新的微观基准测试成为可能。这让我们积累了几十个微观基准测试。我们用这些测试来了解RAMCloud中使用的现有库的性能,同时也用来衡量为RAMCloud编写的新类的性能。
一旦你对什么开销大,什么开销小有了大致的了解,你就可以利用这些信息,尽可能地选择开销小的操作。在很多情况下,一个更高效的方法和一个更慢的方法一样简单。例如,当存储一个将使用键值进行查询的大型对象集合时,你可以使用哈希表或有序map。这两种方法通常在库包中都很是可用的,而且使用起来都很简单、干净。然而,哈希表的速度可以轻松达到5-10倍。因此,你应该总是使用哈希表,除非你需要map所提供的排序属性。
作为另一个例子,考虑在C或C++这样的语言中分配一个结构体数组。有两种方法可以做到这一点。一种方法是让数组持有结构体的指针,在这种情况下,你必须首先为数组分配空间,然后为每个单独的结构体分配空间。将结构体本身存储在数组会更有效率,这样你只需为所有结构体分配一个大块。
如果提高效率的唯一方法是增加复杂性,那么选择就更加困难了。如果更有效的设计只增加了少量的复杂性,而且这些复杂性是隐藏的,所以它不会影响任何接口,那么它可能是值得的(但要注意:复杂性是递增的)。如果更快的设计增加了很多实现的复杂性,或者导致了更复杂的接口,那么一开始用更简单的方法可能更好,如果性能被证明是个问题,再进行优化。然而,如果你有明确的证据表明性能在特定情况下是很重要的,那么你不妨立即实施更快的方法。
在RAMCloud项目中,我们的总体目标之一是为通过数据中心网络访问存储系统的客户机提供尽可能低的延迟。因此,我们决定使用特殊的硬件进行联网,这使得RAMCloud可以绕过内核,直接与网络接口控制器进行通信以发送和接收数据包。尽管这个决定增加了复杂性,但我们还是做出了这个决定,因为我们从之前的测量中知道,基于内核的网络会太慢,无法满足我们的需求。在RAMCloud系统的大部分其他部分中,我们都能够进行简单的设计;把这个大问题 “解决”了,其他的事情就容易多了。
一般来说,简单的代码往往比复杂的代码运行得更快。如果你已经定义好了特殊情况和异常情况,那么就不需要用代码来检查这些情况,系统就会运行得更快。深类比浅类更有效率,因为它们在每次方法调用时都能完成更多工作。浅类会导致更多的层交叉,而每层交叉都会增加开销。
Last updated