概要

本文继续介绍一些常用的性能优化技巧

值拷贝还是地址拷贝

Golang中有些类型在函数参数或返回值传递时,使用值拷贝;比如array,struct。有些类型在函数参数或返回值传递时,使用地址拷贝,比如slice,map,pointer。似乎使用地址拷贝可以降低“数据搬运”的工作量,因此效率更高。

然而实际情况可能并非如此,以array和slice为例做对比,分别传递1024个int的数据,进行bench测试后的结果如下:

go test -v -bench . -benchmem
goos: darwin
goarch: amd64
BenchmarkArray
BenchmarkArray-8   	 1352581	       884 ns/op	       0 B/op	       0 allocs/op
BenchmarkSlice
BenchmarkSlice-8   	  712309	      1433 ns/op	    8192 B/op	       1 allocs/op

从结果来看,使用array进行传递的效率更高。

主要的原因是使用地址拷贝时,数据会被存储在堆上,因此给gc带来了压力。

对于一些短小的对象,复制成本远小于在堆上分配和回收操作。

预设容量

对于map,slice这样的集合类型,预设容量显然性能更好,更极大减少了堆内存分配次数,同时也给gc减轻了压力。

直接存储

同样以slice和map为例,其存储的元素可以是值也可以是指针。直接存储值,远比用指针高效。这不但减少了堆内存分配,关键还在于垃圾回收器不会扫描非指针类型 key/value 或slice对象。

资源及时释放

map

map 不会收缩 “不再使用” 的空间。就算把所有键值删除,它依然保留内存空间以待后用。

因此对于长时间使用的cache,定期换成一个新的效果可能更好。

当我们使用锁时,请确保lock和unlock之间执行的指令最小化。非必须的指令出现在其中时,在高并发的场景会引起性能问题。

文件句柄

而文件句柄一旦使用完成后,需要及早关闭。不及时关闭的文件句柄会有不小的隐式 “资源泄露”,这些不能及时回收的对象,还会会导致 GC 在内的相关性能问题。

golang的一些高级语法

Defer

编译器通过 runtime.deferproc “注册” 延迟调用,除目标函数地址外,还会复制相关参数(包括 receiver)。在函数返回前,执行 runtime.deferreturn 提取相关信息执行延迟调用。这其中的代价自然不是普通函数调用一条 CALL 指令所能比拟的。

接口

使用接口变量,具体函数调用需要等到函数执行时才能确定,因此存在函数寻址的过程,显然它也会花掉额外的时间

反射

通常使用反射的代码性能会比不使用反射低一个数量级左右,除非必须,否则不要使用。