Golang3种数组拷贝方式性能分析
在Go语言中,我们可以使用for、append()和copy()进行数组拷贝,对于某些对性能比较敏感且数组拷贝比较多的场景,我们可以会对拷贝性能比较关注,这篇文件主要是对比一下这三种方式的性能。
测试
测试条件是把一个64KB的字节数组分为64个块进行复制。
测试代码
package test
import (
	"testing"
)
const (
	blocks    = 64
	blockSize = 1024
)
var block = make([]byte, blockSize)
func BenchmarkFori(b *testing.B) {
	a := make([]byte, blocks*blockSize)
	for n := 0; n < b.N; n++ {
		for i := 0; i < blocks; i++ {
			for j := 0; j < blockSize; j++ {
				a[i*blockSize+j] = block[j]
			}
		}
	}
}
func BenchmarkAppend(b *testing.B) {
	a := make([]byte, 0, blocks*blockSize)
	for n := 0; n < b.N; n++ {
		a = a[:0]
		for i := 0; i < blocks; i++ {
			a = append(a, block...)
		}
	}
}
func BenchmarkCopy(b *testing.B) {
	a := make([]byte, blocks*blockSize)
	for n := 0; n < b.N; n++ {
		for i := 0; i < blocks; i++ {
			copy(a[i*blockSize:], block)
		}
	}
}
测试结果
可以看到copy的性能是最好的,当然append的性能也接近copy,for性能较差。
BenchmarkFori-8            19831             52749 ns/op
BenchmarkAppend-8         775945              1478 ns/op
BenchmarkCopy-8           815556              1473 ns/op
原理分析
我们简单分析copy和append的原理。
copy
可以看到最终都会调用memmove()整块拷贝内存,而且是用汇编实现的,因此性能是最好的。
// slicecopy is used to copy from a string or slice of pointerless elements into a slice.
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
	if fromLen == 0 || toLen == 0 {
		return 0
	}
	n := fromLen
	if toLen < n {
		n = toLen
	}
	if width == 0 {
		return n
	}
	size := uintptr(n) * width
	if raceenabled {
		callerpc := getcallerpc()
		pc := funcPC(slicecopy)
		racereadrangepc(fromPtr, size, callerpc, pc)
		racewriterangepc(toPtr, size, callerpc, pc)
	}
	if msanenabled {
		msanread(fromPtr, size)
		msanwrite(toPtr, size)
	}
	if size == 1 { // common case worth about 2x to do here
		// TODO: is this still worth it with new memmove impl?
		*(*byte)(toPtr) = *(*byte)(fromPtr) // known to be a byte pointer
	} else {
		memmove(toPtr, fromPtr, size)
	}
	return n
}
append
append最终会被编译期转换成以下代码,也是调用了memmove()整块拷贝内存,因此其实性能是和copy差不多的。
	  s := l1
	  n := len(s) + len(l2)
	  // Compare as uint so growslice can panic on overflow.
	  if uint(n) > uint(cap(s)) {
	    s = growslice(s, n)
	  }
	  s = s[:n]
	  memmove(&s[len(l1)], &l2[0], len(l2)*sizeof(T))
总结
| 拷贝方式 | 性能 | 适合场景 | 
|---|---|---|
| for | 较差 | 无法使用append和copy的场景,比如类型不同,需要更加复杂的判断等 | 
| copy | 好 | 适合提前已经分配数组容量,且不是尾部追加的方式 | 
| append | 好 | 适合大多数情况,尾部追加 | 
大部分情况下还是建议使用append,不仅性能好,动态扩展容量,而且代码看起来更加清晰!
转载自:https://juejin.cn/post/7134957581801357348