目录
  • 测试
    • 测试代码
  • 测试结果
    • 原理分析
      • copy
    • append
      • 总结

        在Go语言中,我们可以使用forappend()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,不仅性能好,动态扩展容量,而且代码看起来更加清晰!

        声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。