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

转自https://mp.weixin.qq.com/s/X8c6ZIJdBFptYA9CRj6wnA

今天小土给大家带来一篇关于 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跟踪工具。

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

posted @ 2024-06-19 15:32  daniel456  阅读(8)  评论(0编辑  收藏  举报