33 KiB
编写和优化Go代码
本文档概述了编写高性能Go代码的最佳实践。
目前,这是一个视频,幻灯片和博客文章(“awesome-golang-performance”)的链接集合,但我希望它发展成一个更长的书籍格式,其内容是在这里,而不是外部。链接应该被分类到不同的类别中。
虽然有些讨论会针对单个服务进行会更快(高速缓存等),但设计高性能的分布式系统已经超出了这项工作的范围。在监控和分布式系统设计方面已经有很好的文章。它包含一套完全不同的研究和设计权衡。
所有内容将根据CC-BY-SA进行许可。
本书分为不同的部分:
1) 编写高性能软件的基本技巧
* CS 101-level的东西
2) 编写快速软件的技巧
* 关于如何从Go获得最佳效果的Go-specific章节
3) 编写*真正*快速软件的高级技巧
* 当你优化的代码不够快时
我们可以总结这三个部分:
- “不要糊涂”
- “放聪明点”
- “有危险”
何时何地做优化
我先把这个放在第一位,是因为这真的是最重要的一步。你曾经也应该这样做吗?
每个优化都有成本。通常,这个成本是用代码复杂度或认知负载来表示的 - 优化后的代码很少比未优化的版本简单。
但另一方面,我将称之为“优化经济学”。作为程序员,你的时间是宝贵的。你可以为你的项目工作的机会成本,哪些错误需要修复,以及需要添加哪些功能。优化的工作是很有趣的,但并不总是正确的选择。性能是一项功能,但代价和正确性也是如此。
选择最重要的工作。有时它不是一个实际的CPU优化,而是一个用户体验。就像添加进度条一样简单,或者通过在渲染页面后在后台执行计算来提高页面的响应速度。
有时这是显而易见的:在三小时内完成的报告在一小时完成可能不太有用。
仅仅因为容易优化并不意味着它是值得优化的。忽略low-hang的效果是一种有效的发展战略。
把这看作是优化你的时间。
选择要优化的内容,选择何时优化。
澄清“不成熟的优化”报价。97%的时间。把精力放在那些重要的3%的工作。
TPOP:你应该优化吗?“是的,但只有当问题很重要时,程序真的太慢了,并且有一些期望能够在保持正确性,稳健性和清晰度的同时加快速度。”
快速的软件或快速部署。
http://bitfunnel.org/strangeloop 数字显示,假想搜索引擎需要30k机器 @ 1k美元/年,加倍软件的速度可以节省1500万美元/年,即花费整整一年的时间削减1%的开发人员也可以抵消。
在绝大多数情况下,程序的大小和速度不是问题。最简单的优化不必这样做。第二个最简单的优化就是购买更快的硬件。
如果你决定要改变你的程序,请继续阅读。
如何优化
优化工作流程
在介绍具体细节之前,我们先谈谈优化的一般过程。
优化是一种重构的形式。但是,每一步都会提高性能的某些方面:降低CPU,内存使用率和延迟等,而不是改进源代码的某些方面(代码重复,清晰度等)。这种改进通常是以可读性为代价的。这意味着除了一套全面的单元测试(以确保你的更改没有破坏任何内容)之外,你还需要一套很好的基准测试,以确保你的更改对性能产生预期的影响。你必须能够验证你的更改是否真的在降低CPU。有时你认为会改善的变化实际上会变成零或负变化。在这些情况下,务必确保你撤销修复的程序代码。
//
//亲爱的维护者:
//
//当你完成试图“优化”这个程序,
//并且已经意识到了什么可怕的错误,这是,
//请增加以下计数器作为警告
//下一guy:
//
//total_hours_wasted_here = 42
//
你使用的基准测试必须正确,并为代表性工作负载提供可重复的数字。如果单个的运行差异太大,则会使得小的改进更难以发现。你将需要使用benchstat或等效的统计测试,而不能只是用眼睛去看(请注意,使用统计测试无论如何都是一个好主意)。应该记录运行基准测试的步骤,并且应该向存储库提交任何自定义脚本和工具,并提供如何运行它们的说明。要注意需要很长时间才能运行的大型基准测试套件:它会使开发迭代变慢。
还要注意,任何可以测量的东西都可以优化。确保你正在衡量正确的事情。
下一步是决定你正在优化什么。如果目标是改进CPU,那么什么是可接受的速度。你想要将当前的性能提高2倍吗?10倍?你能否说它是“小于时间T的大小为N的问题”?你想减少内存使用量吗?多少钱?对于内存使用情况的变化,可以接受的速度有多慢?你愿意放弃什么来换取较低的空间需求?
优化服务延迟是一个棘手的问题。整本书都是关于如何对Web服务器进行性能测试的。主要问题是对于单线程代码,对于给定的问题大小,性能相当一致。对于webservices,你不会得到一个单一的数字。一个适当的Web服务基准套件将为给定的需求/秒级别提供延迟分布。...(链接到Gil Tene的Talk)
绩效目标必须具体。你会(几乎)总是能够更快地做出一些事情。优化往往是一个收益递减的游戏。你需要知道何时停止。你要付出多少努力才能完成最后一点工作。你愿意做出这样的代码是多么难以维护?
Dan Luu的演讲还指出了粗略计算的优势,以确定你的目标表现数据是否合理。
对于绿地开发,你不应该把所有的基准和性能数字都留到最后。很容易说“我们稍后会修复”,但如果性能非常重要,那么从一开始就将是一个设计考虑因素。在解决性能问题时所需的任何重大体系结构更改在截止日期前将过于冒险。请注意,在开发过程中,重点应放在合理的程序设计,算法和数据结构上。在更低层次的堆栈优化应该等到开发周期晚些时候才能获得更完整的系统性能视图。你在系统不完整时执行的任何完整系统配置文件都会对完成系统中瓶颈的位置给出偏斜视图。
死亡减1000。
编写你可以测试的代码。你可以在较大的系统上执行分析。你可以通过基准测试测试孤立的部分。你需要能够提取并设置足够的环境上下文,以便基准测试足够并具有代表性。
你的目标是什么和目前的表现之间的差异也会让你知道从哪里开始。如果你只需要10%-20%的性能改进,那么可以通过一些实施调整和较小的修复来实现。如果你需要一个10倍或更多的因子,那么用一个左移代替一个乘法不会削减它。这可能会要求你的堆栈上下进行更改。
良好的性能工作需要从系统设计,网络,硬件(CPU,缓存,存储),算法,调整和调试等多个不同层面的知识。在时间和资源有限的情况下,考虑哪个级别能够提供最大的改进:它并不总是算法或程序调优。
一般而言,优化应该从上到下进行。系统级别的优化将比表达级别的影响更大。确保你在适当的水平上解决问题。
本书主要讨论如何减少CPU使用率,减少内存使用量并减少延迟。很高兴指出你很少能做到这三点。也许CPU时间更快,但现在你的程序使用更多的内存。也许你需要减少内存空间,但现在该程序需要更长的时间。
阿姆达尔定律告诉我们要关注瓶颈。如果你将运行时间仅占5%的例程速度提高一倍,那么整个挂钟的速度只有2.5%。另一方面,将80%的时间加速10%的例程将使运行时间提高近8%。配置文件将有助于确定实际花费的时间。
优化时,你想减少CPU必须完成的工作量。Quicksort比气泡排序更快,因为它能以更少的步骤解决相同的问题(排序)。这是一个更高效的算法。你已经减少了CPU完成相同任务所需完成的工作。
像编译器优化一样,程序调优通常只会在整个运行时间中造成一点小小的负担。大的胜利几乎总是来自算法改变或数据结构的改变,这是你的程序组织方式的根本转变。编译器技术有所改进,但速度很慢。Proebsting定律表明,编译器每18 年的性能翻倍,这与摩尔定律(稍微误解了解释)形成鲜明对比,该定律使处理器性能每18 个月翻一番。算法改进在更大的范围内工作。从1991年到2008年,混合器整数规划算法提高了30,000倍 举一个更具体的例子,考虑将Uber博客文章中描述的蛮力地理空间算法替换为更适合所呈现任务的更专门的算法:https: //medium.com/@buckhx/unwinding-uber- S-最高效的服务,406413c5871d
分析器可能会告诉你,大量的时间都花在了特定的例程上。这可能是一个昂贵的例程,或者它可能是一个便宜的例程,只是被称为许多次。而不是立即加快这一过程,看看你是否可以减少被调用的次数或完全消除它。我们将在下一节讨论更具体的优化策略。
三个优化问题:
- 我们必须这样做吗?最快的代码是永远不会运行的代码。
- 如果是的话,这是最好的算法。
- 如果是的话,这是这个算法的最佳实现。
具体的优化技巧
Jon Bentley在1982年的作品“编写高效程序”将程序优化视为一个工程问题:基准。分析。提高。校验。迭代。他的一些技巧现在由编译器自动完成。程序员的工作是使用编译器无法做到的转换。
本书的摘要如下:
- http://www.crowl.org/lawrence/programming/Bentley82.html
- http://www.geoffprewett.com/BookReviews/WritingEfficientPrograms.html
和程序调整规则: https://web.archive.org/web/20080513070949/http://www.cs.bell-labs.com/cm/cs/pearls/apprules.html
在考虑对程序进行更改时,有两个基本选项:你可以更改数据,也可以更改代码。
数据的更改
改变你的数据意味着增加或改变你正在处理的数据的表示(其中一些依赖于改变与数据结构的不同方面相关的O())。
增加数据结构的想法:
-
额外字段:例如,存储链接列表的大小,而不是在询问时迭代。或者将经常需要的其他节点的指针存储到多个搜索中(例如,双向链接列表中的“向后”链接以进行删除O(1))。当你需要的数据便于存储并保持最新时,这些更改很有用。
-
额外的搜索索引:大多数数据结构都是为单一类型的查询而设计的。如果你需要两种不同的查询类型,对数据进行额外的“查看”可能会有很大的改进。例如,[] struct,由ID引用,但有时是string - > map [string] id(或* struct)
-
有关元素的额外信息:例如布隆过滤器。这些数据结构必须小而快,以免压倒其余的数据结构。
-
如果查询很昂贵,请添加一个缓存。我们都熟悉memcache,但还有进程内缓存。
- 通过网络,网络+序列化成本将会受到影响
- 进程内缓存,但现在你需要担心到期
- 即使是单个项目也可以帮助(日志文件时间解析示例)
TODO:“缓存”可能不是键值对,只是指向你工作的地方。这可以像“搜索手指”一样简单
这些都是数据结构层面“做更少工作”的明确例子。他们都花费空间。大多数情况下,如果你针对CPU进行优化,程序将使用更多的内存。这是经典的时空交易:https://en.wikipedia.org/wiki/Space%E2%80%93time_tradeoff
如果你的程序使用太多的内存,也可以换个方式。减少空间使用量以换取更多计算。而不是存储的东西,每次计算它们。你还可以压缩内存中的数据,并在需要时随时对其进行解压缩。
有一本关于减少程序使用空间的在线覆盖技术书。虽然它最初是针对嵌入式开发人员编写的,但这些想法适用于处理大量数据的现代硬件程序。http://www.smallmemory.com/
重新排列你的数据:消除填充。删除额外的字段。更改为较慢的数据结构。跳过指针式树状结构,改用切片和线性搜索。为你的数据定制压缩格式:浮点(go-tsz),整数(delta,xor + huffman)
我们稍后会详细讨论数据布局。
现代计算机和存储器层次结构使空间/时间的权衡不太明确。查找表很容易在内存中“远离”(因此访问成本很高),使得每次需要时重新计算一次值都会更快。
这也意味着基准测试通常会显示由于缓存争用而导致生产系统无法实现的改进(例如,查找表在基准测试期间位于处理器缓存中,但在真实系统中使用时总是会被“真实数据”冲刷。哈希表实际上直接解决了这个问题,比较了满足和无约束的处理器缓存上的性能。参见Jump Hash论文中的图4和图5:https ://arxiv.org/pdf/1406.2294.pdf )
TODO:如何模拟满足的缓存,显示增量成本
另一个要考虑的方面是数据传输时间。通常,网络和磁盘访问非常缓慢,因此能够加载压缩块的速度将比获取数据后解压缩数据所需的额外CPU时间快得多。一如既往,基准。二进制格式通常比文本格式更小且更快解析,但代价是不再是人类可读的格式。
算法的更改
如果你不更改数据,另一个主要选项是更改代码。
最大的改进很可能来自算法变化。这与使用快速排序将气泡排序替换为从O(n ^ 2)排序到O(n log n)或使用映射查找替换通过过去是小O(n)的数组的线性扫描等效(O (1))。
这就是软件如何变慢。最初设计用于一种用途的结构被重新用于未设计的东西。这是逐渐发生的。
直观地掌握不同的大O级别是很重要的。为你的问题选择正确的数据结构。你不必一直刮刮胡须,但是这样做可以防止很久以后才会发现的愚蠢的性能问题。
基本的复杂类别是:
- O(1):字段访问,数组或地图查找
- O(log n)):二分搜索
- O(n):简单循环
- O(n*m):嵌套循环
- O(nlogn):分而治之
- combinatoric - 小心!
链接:bigocheatsheet.com
假设你需要搜索未分类的数据集。“我应该用二分搜索”,你知道一个二分搜索O(log n)比O(n)线性扫描快。但是,二分查找需要对数据进行排序,这意味着你需要先对它进行排序,这将花费O(n log n)时间。如果你正在进行大量搜索,那么分类的前期成本将会得到回报。另一方面,如果你主要做查询,也许有一个数组是错误的选择,你最好支付O(1)查找地图的代价。
选择最简单的合理数据结构并继续。CS 101,编写“不慢的软件”。别傻了。这应该是你的默认开发模式。如果你知道需要随机访问,请不要选择链接列表。如果你知道需要按顺序遍历,请不要使用地图。需求变化,你不能总是猜测未来。对工作量做出合理的猜测。
http://daslab.seas.harvard.edu/rum-conjecture/
类似问题的数据结构在做一件工作时会有所不同。随着插入的发生,二叉树每次排序一次。未排序的数组插入速度更快但未排序:最后,“敲定”你需要一次完成排序。
当编写一个供其他人使用的包时,避免每个用例都要优先考虑的诱惑。这将导致代码不可读。按设计的数据结构实际上是单一用途的。你既不能读懂头脑,也不能预测未来。如果用户说“你的软件包对于这个用例太慢”,一个合理的答案可能是“然后在这里使用这个软件包”。一揽子计划应该“做得很好”。
有时混合数据结构将提供你需要的性能改进。例如,通过分段数据,你可以将搜索范围限制在一个存储桶中。这仍然支付O(n)的理论成本,但常数会更小。当我们进行编程调整时,我们将重新审视这些调整。
在讨论大O符号时,人们忘记了两件事
一:涉及一个恒定的因素。具有相同算法复杂度的两种算法可以具有不同的常数因子。想象一下,在一个列表上循环100次而不是循环一次即使两者都是O(n),也有一个常数因子高出100倍。
这些常数因素是为什么即使合并排序,快速排序和排列所有O(n log n),每个人都使用快速排序,因为它是最快的。它具有最小的常数因子。
第二件事是大O只说“随着n增长到无穷大”。它没有提到小n。“随着数字的增长,这是主导运行时间的增长因素。”
经常有一个分界点,在这个分界点以下,木材算法更快。Go标准库sort包的一个很好的例子。大多数时候它使用快速排序,但是当分区大小降到12个元素以下时,它会进行shell排序传递,然后进行插入排序。
现代计算机中的存储器层次结构将问题混淆了一点,因为高速缓存更喜欢将片段扫描到追踪指针的有效随机访问的可预测访问。不过,最好从一个好的算法开始。我们将在硬件特定部分讨论这个问题。
“这场斗争可能并不总是最强,也不是最快的比赛,但这是打赌的方式。” - 吉卜林。
有时,针对特定问题的最佳算法不是单一算法,而是专门针对稍微不同的输入类的算法集合。这个“polyalgorithm”可以快速检测出需要处理的输入类型,然后发送到相应的代码路径。这就是上面提到的排序包所做的:确定问题的大小并选择不同的算法。在string与bytes包做类似的事情,检测和专门针对不同的情况。与数据压缩一样,你对输入内容的了解越多,定制解决方案就越好。即使优化并不总是适用,通过确定使用和执行不同的逻辑是安全的,使代码复杂化可能是值得的。
sort上面提到的包是多算法的另一个例子。除了结合quicksort,shell排序和插入排序之外,它还会跟踪快速排序的递归深度并在必要时调用堆排序。
基准输入
了解你的每种输入尺寸可能在生产中有多大。
你的基准测试必须使用适当大小的输入。正如我们所看到的,不同的算法在不同的输入大小下都有意义。如果你的预期输入范围<100,那么你的基准应该反映这一点。否则,选择最适合n = 10 ^ 6的算法可能不是最快的。
能够生成有代表性的测试数据。不同的数据分布会在你的算法中引发不同的行为:想想经典的“数据排序时快速排序为O(n ^ 2)”示例。类似地,对于均匀的随机数据,插值搜索是O(log log n),但是O(n)最差的情况。知道你的输入是什么样子是代表性基准和选择最佳算法的关键。如果你用来测试的数据不能代表实际工作负载,那么你可以轻松完成针对某个特定数据集的优化,“过度配置”你的代码以便使用一组特定的输入进行最佳工作。
这也意味着你的基准数据需要代表真实世界。如果重复的请求非常少见,保留它们比重新计算它们更昂贵。如果你的基准数据仅包含相同的重复请求,则缓存将提供不准确的性能视图。
另外请注意,一旦部署到生产环境并在40核心服务器上达到250k /秒,就可以看到笔记本电脑上不明显的一些问题。
编写好的基准测试可能很困难。
程序调整
程序调优曾经是一种艺术形式,但编译器变得更好。所以现在事实证明,编译器可以比复杂的代码更好地直接优化代码。Go编译器在匹配gcc和clang方面还有很长的路要走,但这确实意味着在调整时需要小心,特别是在升级代码时不要变得更糟。一旦编译器得到改进,肯定会出现一些针对缺少特定编译器优化工作的调整。
如果你正在解决特定的运行时或编译器代码生成问题,请始终使用指向上游问题的链接记录你的更改。这可以让你在bug修复后快速重新访问你的优化。
打击基于民间传说的崇拜“性能提示”的诱惑,甚至是从你自己的经验中过度概括。每个性能缺陷都需要根据自身的优点加以处理。即使之前已经有效,确保配置文件确保修复仍然适用。你以前的工作可以指导你,但不要盲目应用以前的优化。
程序调优是一个迭代过程。继续重新访问你的代码并查看可以进行哪些更改。确保你在每一步都取得进展。经常有一项改进可以使其他人获得成功。(现在我没有做A,我可以通过做C来简化B)。这意味着你需要继续观察整个图片,而不是沉迷于一小组线。
一旦你确定了正确的算法,程序调优就是改进算法实现的过程。在Big-O表示法中,这是减少与程序相关的常量的过程。
所有的节目调整都要么让速度变慢,要么减慢速度。算法变化也属于这些类别,但我们将看到较小的变化。你的具体做法随技术变化而变化。
做一个缓慢的事情可能会用更快的散列函数替换SHA1或者hash/fnv1。少做一次缓慢的事情可能会节省一个大文件的哈希计算结果,因此你不必多次执行该操作。
保留意见。如果不需要做什么,请解释原因。通常,在优化算法时,你会发现在某些情况下不需要执行的步骤。记录它们。其他人可能会认为这是一个错误,需要放回去。
空程序立刻给出了错误的答案。如果你不必是正确的,那么很快就会很快。
缓存常见情况:你的缓存甚至不需要很大。优化日志处理脚本以缓存上一次传递给time.parse()以显着加速但要注意缓存失效,线程问题等随机缓存驱逐是快速而充分有效的。 - 只将“一些”项放入缓存(概率上),以便用最少的逻辑将缓存大小限制为流行项目比较缓存逻辑的成本与重新获取数据的成本。大容量缓存可能会增加gc压力并且不断吹动处理器缓存
我已经完成了一个网络跟踪实验,表明即使是最佳的缓存也不值得。你的预期命中率很重要。你需要将比率导出到你的监控堆栈。不断变化的比例将显示流量的变化。然后是重新访问缓存大小或过期策略的时候了。
方案调整:如果可能的话,保留旧各地执行的测试,如果没有可能,产生足够的黄金测试用例来比较输出利用数学身份:
ed6c6c9c11
,
https://gist.github.com/dgryski/67e6a7ff94c3a1add30eb26ec0ad8b0f
乘法加法,(另外,https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf )只是清除你使用的部分,而不是整个数组以最小的步骤完成,一次从浮点数学到整数数学或mandelbrot删除sqrt,或者lttb在更昂贵的检查之前删除廉价检查:例如,在正则表达式之前的strcmp,(qv,查询之前的布隆过滤器)罕见情况之前的常见情况,即避免总是失败的额外测试从内部循环中移除分支仍然有效的展开:https://play.golang.org/p/6tnySwNxG6O
许多关于调优的民间传说性能提示依赖于对编译器的优化不充分,并鼓励程序员手工完成这些转换:将循环中的不变计算,使用移位而不是乘法,循环展开,常见的子表达式消除,......
编译器无法做到的转换依赖于你了解有关算法,输入数据,系统中的不变量以及可以做出的其他假设等事情,并将该隐式知识分解为删除或更改数据结构中的步骤。
每个优化都会对你的数据进行假设。这些必须记录下来,甚至更好地进行测试。这些假设将会在你的程序崩溃,放慢速度,或随着系统发展而开始返回错误数据的地方。
程序调整改进是累积的。5倍3%的改善是15%的改善。进行优化时,值得考虑预期的性能改进。用更快的替换哈希函数是一个不断改进的因素。
了解你的要求和可以改变的地方可以提高性能。在#performance Gophers Slack频道中呈现的一个问题是用于为字符串键/值对映射创建唯一标识的花的数量。最初的解决方案是提取键,对它们进行排序,并将结果字符串传递给散列函数。我们提出的改进解决方案是在键/值添加到地图时对其进行单独散列处理,然后将所有这些散列在一起以创建标识符。
日志解析示例:
- 时间解析很慢
- 添加单个项目缓存是很好的
- 消除时间解析并手动完成一些整数数学运算速度更快
- 一般算法很慢,你可以更快,因为你更了解你的问题
- 但代码与你需要的密切相关; 很难改变
- 优化是专业化的,专用代码比通用代码更易于改变。
对于大多数情况,标准库实现需要“足够快”。如果你有更高的性能需求,你可能需要专门的实现。
定期进行配置以确保跟踪系统的性能特征,并准备随着流量更改而重新优化。了解你的系统的极限,并有好的指标,让你预测什么时候你会达到这些限制。
当你的应用程序的使用发生更改时,不同的部分可能会成为热点。重温先前的优化并决定它们是否仍然值得,并在可能的情况下恢复为更易读的代码。我有一个系统,我使用一组复杂的mmap优化了启动时间,反映了不安全性。一旦我们改变了系统的部署方式,这个代码就不再需要了,我用更可读的常规文件操作取代了它。
优化工作流程摘要
-
所有优化都应遵循以下步骤:
- 1.确定你的表现目标,并确认你没有达到他们的目标
- 2.配置文件来识别要改进的区域。这可以是CPU,堆分配或goroutine阻塞。
- 3.基准测试来确定你的解决方案将使用内置基准测试框架提供的加速( http://golang.org/pkg/testing/ ) 确保你在目标操作系统和体系结构上进行了正确的基准测试。
- 4.之后再次进行配置以验证问题已消失
- 5.使用 https://godoc.org/golang.org/x/perf/benchstat或 https://github.com/codahale/tinystat 来验证一组时间“充分”不同,以便优化值得添加代码复杂性。
- 6.使用 https://github.com/tsenart/vegeta 负载测试HTTP服务(+其他花哨的人)
- 7.确保你的延迟数字有意义: https://youtu.be/lJ8ydIuPFeU
第一步很重要。它会告诉你何时何地开始优化。更重要的是,它还会告诉你何时停止。几乎所有优化都会增加代码的复杂性以换取速度。而且你总是 可以更快地编写代码。这是一个平衡的艺术。
工具
介绍性分析
一般适用于源代码的技术
- 1.介绍pprof
- Go工具pprof(https://github.com/google/pprof)
- 2.编写和运行(微)基准
- 简介,将hot code提取到基准,优化基准,配置文件。
- -cpuprofile/-memprofile/-benchmem
- 0.5 ns/op意味着它被优化了 ->如何避免
- 编写好的基准测试的技巧(删除不必要的工作,但增加基准)
- 3.如何读取它的pprof输出
- 4.显示的运行系统有哪些不同的部分
- 5.宏观基准(生产剖析)
- net/HTTP/pprof
追踪
垃圾收集
你不止一次支付内存分配。第一个显然是你分配它的时候。但是,每次垃圾收集运行时,你也要付钱。
减少回收再利用。 - @bboreham
- 堆栈与堆分配
- 什么导致堆分配?
- 了解逃逸分析(和当前的限制)
- /debug/pprof/heap和-base
- API设计限制分配:允许传入缓冲区,因此调用者可以重用而不是强制分配
- 你甚至可以在扫描时仔细修改切片
- 减少指针以减少gc扫描时间
- 无指针的map键
- GOGC
- 缓冲区重用(sync.Pool vs或通过go-slab等自定义)
运行时和编译器
- 通过接口调用的成本(在CPU级别上的间接调用)
- runtime.convT2E/runtime.convT2I
- 类型断言与类型切换
- 延缓
- 用于整数,字符串的特殊映射实现
- 边界检查消除
- []字节<->字符串副本,地图优化
Unsafe
- 它所有的危险项
- unsafe的常见用途
- mmap数据文件
- 结构填充
- 快速反序列化
- 字符串< - >切片转换,[]字节< - > [] uint32,...
与标准库共同陷阱
- time.After()泄漏,直到它被触发
- 重用HTTP连接...
- ....
- rand.Int()和朋友是1)互斥体保护和2)创建昂贵
- 考虑交替随机数生成(go-pcgr,xorshift)
- binary.Read和binary.Write使用反射并且很慢; 手动做。
替代实现
- 标准库软件包的普遍替代品:
- encoding/json - > ffjson
- net/http - > fasthttp(但不兼容的API)
- regexp - > ragel(或其他正则表达式包)
- 系列化
- encoding/gob - > https://github.com/alecthomas/go_serialization_benchmarks
- protobuf - > https://github.com/gogo/protobuf
- 所有格式都有权衡:选择一种符合你需要的编码空间,解码速度,语言/工具兼容性......
- database/sql - > jackx/pgx,...
- gccgo
CGO
- cgo调用的性能特征
- 降低成本的技巧:配料
- Go和C之间传递指针的规则
- syso文件
高级技术
- 特定于运行代码的体系结构的技术
- CPU缓存介绍
- 性能悬崖
- 围绕缓存行构建直觉:大小,填充,对齐
- 共享假
- 真正的共享 - >分片
- OS工具来查看缓存未命中
- 地图与切片
- SOA vs AOS布局
- 减少指针追逐
- 分支预测
- 函数调用开销
- 关于Jeff Dean的2002年数字(加上更新)的评论
- cpus变得更快了,但是内存没有跟上
部件
- 关于为Go编写汇编代码的东西
- 编译器改进; bar很高
- 尽可能少地替换以产生影响
- 非常重要的基准:改进可能是巨大的(高速公路的10倍)零(小点),或甚至更慢(不内联)
- 总是有纯粹的Go版本(noasm build tag):测试,arm,gccgo
- 简要介绍语法
- 调用约定
- 使用不受asm支持的操作码
- 关于为什么内在性很难的笔记
- 所有的工具,使这更容易:asmfmt,peachpy,c2goasm,...
工具
看一些更有趣的/先进的工具
- perf(perf2pprof)
优化整个服务
大多数情况下,你不会看到一个CPU限制的例程。这是一个简单的例子。如果你有优化服务,则需要查看整个系统。监测。指标。随着时间的推移记录很多事情,这样你可以看到它们变得更糟,所以你可以看到你的更改对生产的影响。
tip.golang.org/doc/diagnostics.html
- 系统设计参考:SRE Book,实用的分布式系统设计
- 额外的工具:更多日志记录+分析
- 两条基本规则:加速缓慢的事情或减少频率。
- 分布式跟踪以追踪更高级别的瓶颈
- 用于查询单个服务器而不是批量查询模式
附录:实施研究论文
实施论文的提示:( algorithm另请参阅data structure)
-
从明显的解决方案和合理的数据结构开始。
-
“现代”算法往往具有较低的理论复杂性,但具有较高的常数因子和很多实施复杂性。
最快的算法经常可以被几乎一样快速且容易理解的算法取代——Douglas W. Jones University of Iowa
Treap与RB/AVL树相比,Raft更容易理解Paxos斐波那契堆出众的难度,并且拥有巨大的不变系数
增加的复杂性必须足以使得回报实际上值得。缓存驱逐算法就是一个很好的例子。不同的算法可以具有高得多的复杂度,仅命中率的小改进。当然,你可能无法在测试之前进行测试,直到你有一个可行的实施并将其整合到你的程序中。
有时候这篇论文会有图表,但很像只发布正面结果的趋势,但这些倾向往往会偏向于表示新算法的优点。
-
选择正确的纸张。
-
寻找他们的算法声称击败和实施的论文。
早期的论文通常会更容易理解,并且必然会有更简单的算法。
并非所有的文件都很好。
查看论文写入的上下文。确定有关硬件的假设:磁盘空间,内存使用情况等。一些较旧的论文在70年代或80年代进行了合理的不同折衷,但不一定适用于你的使用案例。例如,一些流媒体算法是为路由器硬件而设计的,这可能会使转换成软件变得非常痛苦。
-
确保算法对数据保持的假设。
这将需要一些挖掘。你可能不想实现你找到的第一篇论文。
-
确保你了解算法。这听起来很明显,但是否则无法进行调试。 https://blizzard.cs.uwaterloo.ca/keshav/home/Papers/data/07/paper-reading.pdf
-
一个良好的理解可能会让你从论文中提取关键的想法,并且可能将这个想法应用于你的问题,这可能比重新实现整个事情更简单。
数据结构或算法的原始文件并不总是最好的。后来的论文可能会有更好的解释。
-
一些论文发布了你可以比较的参考源代码,但是
- 1)学术代码几乎普遍可怕
- 2)谨防许可限制
- 3)当心错误
还要关注GitHub上的其他实现:它们可能与你的bug相同(或不同) https://www.youtube.com/watch?v=8eRx5Wo3xYA http://codecapsule.com/2012/01/18/how-to-implement-a-paper/