Go中的一些优化笔记,简约而不简单

共 3180字,需浏览 7分钟

 ·

2022-07-17 17:30

今天小土给大家带来一篇关于 Golang 项目中最简单的优化的文章。原文见 Golang: simple optimization notes[1]

我们这里简单聊一下优化本身,然后我们直接从实际的示例开始。

为什么要优化呢?

当你资源占有较高的话会需要很大的成本,虽然现在服务器资源也不是很贵,但是你还是需要针对的做一些优化工作。

另外每个优化应该建立在一个benchmark的基础上,需要体现它给我们带来多大的收益。

下面主要从slice、string、struct、function、map、interface、channel、pointer等方面罗列了一些常见的优化点。

数组和slice优化篇

提前为slice分配内存

尽量使用第三个参数: make([]T, 0, len)

如果你事先不知道确切的数量并且slice是临时的,你可以设置得大一些,只要slice在运行时不会增长。

不要忘记使用“copy”

我们尽量不要在复制时使用 append,例如,在合并两个或多个slice时。

正确地使用迭代

如果我们有一个包含很多元素或比较大的元素的slice,我们会尝试使用“for”或 range 单个元素。通过这种方法,可以避免不必要的复制。

学会复用slice

如果我们需要对传入的slice进行某种操作并返回结果,我们可以直接return,但已经修改了。这样我们就可以避免了新的内存分配。

不要留下未使用的slice

如果我们需要从slice中切下一小块并仅使用它,其实主要部分也会保留下来。可以使用copy产生一个新的slice,而旧的对象让GC回收。

string-字符串优化篇

正确地进行拼接

如果拼接字符串可以在一个语句中完成,那么可以使用“+”,如果需要在循环中执行此操作,那么可以使用string.Builder。通过“Grow”也可以预先指定builder的大小。

使用转换优化

由于字符串是由字节组成的,因此有时这两种类型之间的转换可以避免内存分配。

使用池化技术

我们可以池化字符串,从而帮助编译器只存储一次相同的字符串。

避免内存分配

我们可以使用map来替代复合键,我们也可以使用[]byte。尽量不要使用fmt包,因为它的所有函数都使用了反射。

struct-结构体优化篇

避免复制大的struct

我们理解的小struct,是指不超过 4 个字段的struct,不超过一个机器字

标准的copy案例

  • 转换成interface
  • 接收和发送到channel
  • 替换map中的item
  • 向slice添加元素
  • 迭代(range)

避免通过指针来访问struct中的字段

解引用是比较昂贵的,我们可以尽量少做,尤其是在循环中。我们也会失去使用快速寄存器的能力。

使用小型的struct

这项工作由编译器优化的,这意味着它的工作量很小。

通过内存对齐来减小struct大小

我们可以对齐struct(根据字段的大小,以正确的顺序排列),从而可以减小struct本身的大小。

func-函数优化篇

使用内联函数或自己内联

我们尽量编写一些可供编译器内联的小函数——它很快,但自己从函数中嵌入代码则更快。对于热路径函数尤其如此。

什么情况下不会被内联?

  • recovery 函数
  • select
  • 类型声明
  • defer
  • goroutine
  • for-range

明智地选择你的函数参数

我们尽量使用“小”参数,因为它们的拷贝会被特别优化。我们也尝试在拷贝和GC的负载的与增长堆栈之间保持平衡。避免使用大量的参数——让你的程序使用超快速的寄存器(寄存器的数量是有限的)

声明一个命名好的return结果

这似乎比在函数体中声明这些变量更高效。

保存函数中间的结果

帮助编译器优化你的代码,保存中间结果,然后会有更多的选择来优化你的代码。

谨慎使用“defer”

尽量不要使用 defer,或者至少 不要在循环中使用defer

为“hot path”提供便利

避免在这些地方分配内存,尤其是短期对象。首先要检查的的就是最常见的分支(if,switch)。

这里 hot path在Go源码中[2]也出现多次,根据在 sync.Once 的上下文中,“hot path”是什么意思?[3]中的回答,这里翻译为热路径是非常频繁执行的指令序列。

map优化篇

提前分配内存

一切都和其他地方一样。初始化map时,指定其大小。

使用空结构作为值

struct{}什么都不是,因此例如对信号值使用这种方法是非常有益的。

清空map

map只能增长,不能缩小。我们需要控制这一点——完全而明确地重置map。因为删除其所有元素无济于事。

尽量不要在键和值中使用指针

如果 map 不包含指针,那么 GC 就不会在它上面浪费宝贵的时间。而且要知道字符串也是指针——使用[]byte而不是字符串作为键。

减少更改的次数

同样,我们不想使用指针,但我们可以使用 map 和 slice 的复合体,并将键存储在 map 中,将可以不受限制地更改的值存储在slice中。

interface优化篇

计算内存分配

请记住,要给一个接口赋值,你首先需要将其拷贝到某处,然后粘贴一个指针。关键字是拷贝。事实证明,装箱和拆箱的成本将近似于结构体的大小和一次分配。

选择最佳的类型

在某些情况下,装箱/拆箱期间不会进行内存分配。例如,比较小的和布尔值的变量和常量、具有一个简单字段的struct、指针(包括map、chan、func

避免内存分配

与其他地方一样,我们尽量避免不必要的内存分配。例如,将一个接口分配给一个接口,而不是装箱两次。

仅在需要时使用

避免在小型、频繁调用的函数的参数和结果中使用接口。我们不需要额外的包装和拆包。减少使用接口方法调用的频率,哪怕只是因为它可以防止内联。

指针、chan、BCE(Bounds Check Elimination-边界检查) 优化篇

避免不必要的解引用

尤其是在循环中,因为事实证明它太昂贵了。解引用是我们不想以牺牲自己为代价执行的一系列必要操作。

channel使用效率是低效的

使用channel会比其他同步方法慢。另外,select 中的 case 越多,我们的程序就越慢。但是select、case + default是优化过了的。

尽量避免不必要的边界检查

这也很昂贵,我们应该尽一切可能避免它。例如,一次检查(获取)最大slice索引比多次检查更正确。最好是立即尝试获得极端的选项。

总结

在这篇文章中,我们看到了一些相同的优化规则。

帮助编译器做出正确的决定。在编译时分配内存,使用中间结果,并尽量保持代码的可读性。

不要忘记使用内置的分析和trace跟踪工具。

最后小土也祝你在优化的路上做到尽善尽美。

参考资料

[1]

Golang: simple optimization notes: https://medium.com/scum-gazeta/golang-simple-optimization-notes-70bc64673980

[2]

hot path在Go源码中: https://cs.opensource.google/search?q=%22hot%20path%22&ss=go%2Fgo

[3]

在 sync.Once 的上下文中,“hot path”是什么意思?: https://stackoverflow.com/questions/59174176/what-does-hot-path-mean-in-the-context-of-sync-once



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

浏览 8
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报