程序调整

程序调优曾经是一种艺术形式,但编译器变得更好。所以现在事实证明,编译器可以比复杂的代码更好地直接优化代码。Go编译器在匹配gcc和clang方面还有很长的路要走,但这确实意味着在调整时需要小心,特别是在升级Go版本时不要变得更糟。一旦编译器得到改进,肯定会出现一些针对缺少特定编译器优化工作的调整。

TODO:https : //github.com/golang/go/commit/9eb219480e8de08d380ee052b7bff293856955f8)

如果你正在解决特定的运行时或编译器代码生成问题,请始终使用指向上游问题的链接记录你的更改。这可以让你在bug修复后快速重新访问你的优化。

打击基于民间传说的崇拜“性能提示”的诱惑,甚至是从你自己的经验中过度概括。每个性能缺陷都需要根据自身的优点加以处理。即使之前已经有效,确保配置文件确保修复仍然适用。你以前的工作可以指导你,但不要盲目应用以前的优化。

程序调优是一个迭代过程。继续重新访问你的代码并查看可以进行哪些更改。确保你在每一步都取得进展。经常有一项改进可以使其他人获得成功。(现在我没有做A,我可以通过做C来简化B)。这意味着你需要继续观察整个图片,而不是沉迷于一小组线。

一旦你确定了正确的算法,程序调优就是改进算法实现的过程。在Big-O表示法中,这是减少与程序相关的常量的过程。

所有的节目调整都要么让速度变慢,要么减慢速度。算法变化也属于这些类别,但我们将看到较小的变化。你的具体做法随技术变化而变化。

做一个缓慢的事情可能会用更快的散列函数替换SHA1或者hash/fnv1。少做一次缓慢的事情可能会节省一个大文件的哈希计算结果,因此你不必多次执行该操作。

保留意见。如果不需要做什么,请解释原因。通常,在优化算法时,你会发现在某些情况下不需要执行的步骤。记录它们。其他人可能会认为这是一个错误,需要放回去。

空程序立刻给出了错误的答案。
如果你不必是正确的,那么很快就会很快。

“正确性”可以取决于问题。启发式算法大多数情况下是正确的,大部分时间都可以很快,而且猜测和改进的算法可以让您在达到可接受的限制时停下来。

缓存常见情况:

  • 你的缓存甚至不需要很大。
  • 参见下面的 time.Parse()例子; 只有一个价值观产生了影响
  • 但要注意缓存失效,线程问题等。
  • 随机缓存驱逐是快速且足够有效的。
  • 随机缓存插入可以用最少的逻辑将缓存限制为流行的项目。
  • 将缓存逻辑的成本与重新获取数据的成本进行比较。
  • 大容量缓存可能会增加GC压力并不断吹动处理器缓存。
  • 在极端情况下(很少或没有驱逐,将所有请求缓存到一个昂贵的函数),这可以变成记忆

我已经完成了一个网络跟踪实验,表明即使是最佳的缓存也不值得。你的预期命中率很重要。你需要将比率导出到你的监控堆栈。不断变化的比例将显示流量的变化。然后是重新访问缓存大小或过期策略的时候了。

程序调优:

程序调优是以小步骤迭代改进程序的艺术。Egon Elbre列出了他的程序:

  • 提出一个假设,为什么你的程序很慢。
  • 拿出N个解决方案来解决它
  • 尝试一切,并保持最快。
  • 以防万一。
  • 重复。

调整可以采取多种形式。

  • 如果可能,请保留旧的实现以进行测试。
  • 如果不可能,则生成足够的黄金测试用例来比较输出。
    “足够”意味着包括边缘案例,因为这些可能会受到调优的影响,因为您旨在提高一般情况下的性能。
  • 利用数学身份:
    https://github.com/golang/go/commit/ed6c6c9c11496ed8e458f6e0731103126ce60223
    https://gist.github.com/dgryski/67e6a7ff94c3a1add30eb26ec0ad8b0f
    • 与加法相乘
    • 使用WolframAlpha,Maxima,sympy和类似工具来专门化,优化或创建查找表
      (另外,https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf)
    • “只为你使用的东西付费,而不是你可以使用的东西”
    • 零只是数组的一部分,而不是整个事物
    • 最好以微小的步骤完成,一次只做几个陈述
    • 从浮点数学到整数数学
    • 或者mandelbrot删除sqrt,或者lttb删除abs, a < b/c=>a * c < b
    • 在更昂贵的支票前进行廉价支票
    • 例如,在正则表达式之前的strcmp,(qv,在查询之前的布隆过滤器)“少花费更多时间”
    • 在罕见情况之前的常见情况,即避免总是失败的额外测试
    • 展开仍然有效:https://play.golang.org/p/6tnySwNxG6O
    • 代码大小。vs分支测试开销
    • 使用偏移而不是切片分配可以帮助进行边界检查,数据依赖性和代码生成(少于在内部循环中复制)。
    • 这就是Hacker’s Delight的一部分
    • 考虑不同的数字表示法:定点,浮点,(小)整数,
    • 爱好者:带误差累加器的整数(如Bresenham的线和圆),多基数/冗余数字系统

许多针对调优的民间传说性能提示依赖于对编译器的优化不足,并鼓励程序员手动完成这些转换。编译器一直在使用更新,而不是用15年的时间乘以或除以2的幂 - 现在没有人应该亲自去做。类似地,提升循环中的不变计算,基本循环展开,常见子表达式消除等等都是由gcc和clang等自动完成的。Go的编译器完成了其中的许多工作,并继续改进。一如往常,在提交新版本之前进行基准测试。

编译器无法做到的转换依赖于你了解有关算法,输入数据,系统中的不变量以及可以做出的其他假设等事情,并将该隐式知识分解为删除或更改数据结构中的步骤。

每个优化都会对你的数据进行假设。这些必须记录下来,甚至更好地进行测试。这些假设将会在你的程序崩溃,放慢速度,或随着系统发展而开始返回错误数据的地方。

程序调整改进是累积的。5倍3%的改善是15%的改善。进行优化时,值得考虑预期的性能改进。用更快的替换哈希函数是一个不断改进的因素。

了解你的要求和可以改变的地方可以提高性能。在# performance Gophers Slack频道中呈现的一个问题是用于为字符串键/值对映射创建唯一标识的花的数量。最初的解决方案是提取键,对它们进行排序,并将结果字符串传递给散列函数。我们提出的改进解决方案是在键/值添加到地图时对其进行单独散列处理,然后将所有这些散列在一起以创建标识符。

这是一个专业化的例子。

假设我们正在处理一天中的大量日志文件,并且每行都以时间戳开始。

Sun 4 Mar 2018 14:35:09 PST <………………………>
对于每行,我们调用time.Parse()把它变成一个格式。如果性能分析显示我们time.Parse()是瓶颈,那么我们有几种方法可以加快速度。

最简单的方法是保留先前看到的时间戳和相关历元的单项缓存。只要我们的日志文件在一秒钟内有多行,这将是一场胜利。对于1000万行日志文件的情况,这种策略将昂贵的呼叫数量time.Parse()从10,000,000减少到86400 - 每个独立的秒钟一个。

TODO:单项缓存的代码示例

我们可以做更多吗?因为我们确切知道时间戳的格式, 并且它们都在一天内完成,所以我们可以编写自定义时间解析逻辑,将其考虑在内。我们可以计算午夜的时代,然后从时间戳字符串中提取小时,分钟和秒 - 它们都将在字符串中处于固定偏移量 - 并执行一些整数运算。

TODO:字符串偏移版本的代码示例

在我的基准测试中,这将解析时间从275ns / op减少到5ns / op。(当然,即使在275 ns / op下,你也更有可能在I / O上被阻塞,而不是在时间解析上被CPU阻塞。)

一般算法很慢,因为它必须处理更多的案例。你的算法可以更快,因为你更了解你的问题。但是代码与您需要的密切关系更紧密。如果时间格式发生变化,更新更加困难。

优化是专业化的,专用代码比通用代码更易于改变。

对于大多数情况,标准库实现需要“足够快”。如果你有更高的性能需求,你可能需要专门的实现。

定期进行配置文件以确保跟踪系统的性能特征,并准备随着流量变化重新优化。了解你的系统的极限,并有好的指标,让你预测什么时候你会达到这些限制。

当你的应用程序的使用发生更改时,不同的部分可能会成为热点。重温先前的优化并决定它们是否仍然值得,并在可能的情况下恢复为更易读的代码。我有一个系统,我使用一组复杂的mmap优化了启动时间,反映了不安全性。一旦我们改变了系统的部署方式,这个代码就不再需要了,我用更可读的常规文件操作取代了它。

优化工作流程摘要
所有优化都应遵循以下步骤:

  1. 确定你的表现目标,并确认你没有达到他们的目标
  2. 配置文件来识别要改进的区域。
  3. 这可以是CPU,堆分配或goroutine阻塞。
  4. 基准来确定您的解决方案使用内置基准测试框架提供的加速http://golang.org/pkg/testing/
  5. 确保您在目标操作系统和体系结构上进行正确的基准测试。
  6. 之后再次进行配置以验证问题已消失
  7. 使用https://godoc.org/golang.org/x/perf/benchstathttps://github.com/codahale/tinystat来验证一组时间“充分”不同,以便优化值得添加代码复杂性。
  8. 使用https://github.com/tsenart/vegeta负载测试http服务(+其他花哨的:k6,fortio,…)
  9. 确保你的延迟数字是有意义的
  10. 第一步很重要。它会告诉您何时何地开始优化。更重要的是,它还会告诉你何时停止。几乎所有优化都会增加代码的复杂性以换取速度。而且你总是可以更快地编写代码。这是一个平衡的行为。