go语言中值拷贝的成本
在go语言中,值拷贝是常有的事情。赋值,传参和发送值给channel都有值拷贝。本文将讨论各种类型的值拷贝成本。
值的大小
大小是指值的直接部分在内存中占用的字节数。值的非直接部分不会影响值的大小。
在go语言中,如果两个值类型相同并且他们的类型不属于string,interface,数组,struct,那么他们的大小相等。
事实上,对应标准的go编译器和运行时,两个字符串类型的值大小是一样的。两个interface类型的值也是如此。
到目前为止,特定类型的值大小总是一致的。因此,通常,我们将值的大小称为值的类型的大小。
数组类型的值大小由数组中的元素类型的大小和数组的长度决定。数组类型的大小是数组长度乘以数组元素的大小。
struct的大小取决于它所有字段的的大小和顺序。因为在两个相邻的struct字段之间可能会插入一些填充字节,以保证这些字段的某些内存地址对齐要求,因此,struct的大小必须大于或者等于(并且通常大于)其字段的相应类型大小的总和。
下表列出了各种类型的值大小(对于标准Go编译器版本1.12)。在表中,一个词表示一个本地字,在32位架构上为4个字节,在64位架构上为8个字节。
Kind Of Types | Value Size | Required By Go Specification |
---|---|---|
bool | 1 byte | not specified |
int8, uint8 (byte) | 1 byte | 1 byte |
int16, uint16 | 2 bytes | 2 bytes |
int32 (rune), uint32, float32 | 4 bytes | 4 bytes |
int64, uint64, float64, complex64 | 8 bytes | 8 bytes |
complex128 | 16 bytes | 16 bytes |
int, uint | 1 word | architecture dependent, 4 bytes on 32bits architectures and 8 bytes on 64bits architectures |
uintptr | 1 word | large enough to store the uninterpreted bits of a pointer value |
string | 2 words | not specified |
pointer | 1 word | not specified |
slice | 3 words | not specified |
map | 1 word | not specified |
channel | 1 word | not specified |
function | 1 word | not specified |
interface | 2 words | not specified |
struct | the sum of sizes of all fields + number of padding bytes | a struct type has size zero if it contains no fields that have a size greater than zero |
array | (element value size) * (array length) | an array type has size zero if its element type has zero size |
值拷贝的成本
一般而言,复制值的成本与值的大小成比例。但是,值大小并不是决定值复制成本的唯一因素。不同的CPU架构可以专门针对具有特定大小的值优化值复制。
在实践中,我们可以将尺寸小于不大于四个原始单词的值视为小尺寸值。复制小尺寸值的成本很小。
对于标准的Go编译器,除了大型结构和数组类型的值之外,Go中的其他类型都是小型类型。
为了避免在传参和channel值的接受和发送中的巨大的值拷贝操作,我们应该尝试避免大尺寸的struct和数组类型作为函数和方法的参数以及channel的元素,我们可以使用对应类型的指针类型作为参数。
另一方面,我们还应该考虑这样一个事实:太多的指针会增加垃圾收集器在运行时的压力。因此,是否应该使用大型结构和数组类型或它们相应的指针类型取决于具体情况。
通常,在实践中,我们很少使用基类型是slice,channel,map,function类型,string类型和interface类型的指针类型。复制这些基类型的值的成本非常小。
如果元素类型是大型类型,我们还应该尽量避免使用两次迭代变量形式来迭代数组和切片元素,因为每个元素值将被复制到迭代过程中的第二个迭代变量。
以下是对切片元素迭代的不同方式进行基准测试的示例。
package main
import "testing"
type S struct{a, b, c, d, e int64}
var sX, sY, sZ = make([]S, 1000), make([]S, 1000), make([]S, 1000)
var sumX, sumY, sumZ int64
func Benchmark_Loop(b *testing.B) {
for i := 0; i < b.N; i++ {
sumX = 0
for j := 0; j < len(sX); j++ {
sumX += sX[j].a
}
}
}
func Benchmark_Range_OneIterVar(b *testing.B) {
for i := 0; i < b.N; i++ {
sumZ = 0
for j := range sY {
sumZ += sY[j].a
}
}
}
func Benchmark_Range_TwoIterVar(b *testing.B) {
for i := 0; i < b.N; i++ {
sumY = 0
for _, v := range sY {
sumY += v.a
}
}
}
在测试文件的目录中运行基准测试,我们将得到类似于的结果:
Benchmark_Loop-4 500000 3228 ns/op
Benchmark_Range_OneIterVar-4 500000 3203 ns/op
Benchmark_Range_TwoIterVars-4 200000 6616 ns/op
我们可以发现,两次迭代变量形式的效率远低于其他两种形式。