优化GO程序的方法 | 青训营

40 阅读5分钟

优化工作流

在我们讨论具体问题之前,让我们先谈谈优化的一般过程。

优化是重构的一种形式。只不过这种重构过程不是出于改善源代码的代码重复或清晰这些方面,而是为了改善性能:降低 CPU、内存使用、延迟等。这种改进通常是以可读性为代价的。这意味着,除了一套完整且全面的单元测试(以确保你的改动没有破坏任何逻辑),还需要一套好的基准测试,以确保改动对性能产生预期的影响。必须能够验证代码修改是否真的降低了CPU。有时候,你认为会提高性能的改变实际上会变成零或负的改变。在这种情况下,一定要记得撤消你的修改。

What is the best comment in source code you have ever encountered? - Stack Overflow[2]:

//
// Dear maintainer:
//
// Once you are done trying to 'optimize' this routine,
// and have realized what a terrible mistake that was,
// please increment the following counter as a warning
// to the next guy:
//
// total_hours_wasted_here = 42
//

你所使用的基准必须是正确的,并在有代表性的工作负载上提供可重复的数字。如果单个运行的差异太大,会使小的改进更难发现。需要使用benchstat[3]或同类的统计测试工具,而不能只靠单次或者肉眼对比。(注意,无论如何,使用统计测试是一个好主意。)运行基准的步骤应该被记录下来,任何定制的脚本和工具都应该被提交到代码仓库里,并说明如何运行它们。要注意运行时间较长的大型基准套件:这会使你的开发迭代被拖累速度。

还要注意,任何可以测量的指标都可以被优化。请确保你使用的是正确的指标。

下一步是决定你的优化目标是什么。如果目标是提高 CPU 使用效率,什么是可接受的速度?你想把当前的性能提高 2 倍?10 倍? 你能把它表述为 "在少于时间 T 的情况下解决一个大小为 N 的问题 "吗?你是想减少内存的使用吗?减少多少?慢多少是可以接受的?你愿意放弃什么来换取更低的空间需求?

对服务延迟的优化是一个更棘手的问题。关于如何测试网络服务器的书已经写了一整本。主要的问题是,对于一个单一的功能,在给定的问题规模下,性能是相当一致的。对于网络服务,无法用单一的数字表示性能。一个靠谱的网络服务基准测试套件将为给定的 reqs/second 压力提供延迟分布结果。这个讲座对一些问题做了很好的概述。

数据修改

改变你的数据意味着修改你的业务数据字段。从性能的角度来看,这些修改会影响后后续业务逻辑处理数据的时间复杂度。这个过程可能包含对你的数据提前进行一些预处理,以降低后续的数据处理负担。

扩充你的数据结构的一些可能的做法:

  • 冗余字段

    这方面的典型例子是将一个链表的长度存储在头节点的一个字段中。保持它的更新需要更多的工作,但随后查询长度就变成了一个简单的字段查找,而不是一个O(n)的遍历过程。你的数据结构优化可能是这样的:在一些操作中增加一次额外的记录操作,换取高频使用场景下的更快的性能。

    类似地,存储指向经常访问的节点的指针,而不是执行额外的搜索。这涵盖了像双链接列表中的 "next" 链接,以使节点移除变成 O(1) 时间复杂度。一些跳表(skiplist)保留了一个"search finger",在这个位置保存了你上一次查询到的位置。这种优化是假设了下次查询从这个位置开始更好。

  • 冗余的搜索索引

    大多数数据结构都是为单一类型的查询而设计的。如果你需要两种不同的查询类型,在你的数据上有一个额外的 "视图" 可能就有很大的改进。例如,一个数据结构数组可能有一个主键 ID(整数),可以被用来在切片中查询,但还需要用一个次要的 ID(字符串)来查询。你可以用一个从字符串到 ID 或直接到结构本身的映射来增强你的数据结构,而不是在切片上进行迭代。

  • 额外的元素信息

    例如,保留一个你已经插入的所有元素的 Bloomfilter 可以让你快速返回查询 "不匹配"。bloomfilter 的设计应该是“小而快”,不要超过你主要的数据结构的存储成本。(如果主数据结构中的查找成本较低,bloomfilter 的维护成本可能超过这个查询成本)。

  • 如果查询成本很高,增加 cache 层

    在应用层,增加进程内、进程外(如 memcache) 缓存对提升查询效率有很大帮助。对于单个数据结构来说可能这样可能有点夸张,