Go 的值拷贝代价
值拷贝在 Go 语言编程中普遍发生. 赋值, 参数传递, channel 值发送以及接收操作都会涉及值拷贝. 本文将讨论 Go 中的值拷贝代价.
值大小
值的大小意味着该值(直接部分)将在内存中占用多少字节. 值的间接底层部分不影响值的大小.
在 Go 中, 如果两个值的类型属于同一类型, 并且类型不是基本类型, 字符串类型, 接口类型, 数组类型和结构体类型, 那么这两个值的大小总是相等.
事实上, 对于标准的 Go 编译器/运行时, 两个字符串值的大小也总是相等, 对于两个接口值的大小也是一样的.
对于标准的 Go 编译器/运行时, 相同类型的值具有相同的值大小. 因此, 我们通常将值的大小称为值类型的大小.
数组类型的大小取决于元素类型的大小和数组类型的长度. 数组类型大小是数组元素类型大小与数组长度的乘积.
结构类型的大小取决于它的所有字段. 因为在两个相邻字段之间可能存在一些填充字节, 所以结构类型大小不小于(并且通常大于)结构字段的相应类型大小之和.
下表列出了各种类型的值大小. 在表中, 一个字表示一个本地字, 它在 32 位操作系统上是 4 字节, 在 64 位操作系统上是 8 字节.
类型 | Go 1.10 的值大小 | Go 规范要求 |
---|---|---|
bool | 1 字节 | 未明确 |
int8, uint8(byte) | 1 字节 | 1 字节 |
int16, uint16 | 2 字节 | 2 字节 |
int32 (rune), uint32, float32 | 4 字节 | 4 字节 |
int64, uint64, float64, complex64 | 8 字节 | 8 字节 |
complex128 | 16 字节 | 16 字节 |
int, uint | 1 字 | 体系结构相关, 4 或 8 个字节 |
uintptr | 1 字 | 足够大以存储指针值的未解释位 |
string | 2 字 | 未明确 |
pointer | 1 字 | 未明确 |
slice | 3 字 | 未明确 |
map | 1 字 | 未明确 |
channel | 1 字 | 未明确 |
function | 1 字 | 未明确 |
interface | 2 字 | 未明确 |
struct | 所有字段的大小总和 + 填充字节数 | 如果结构体类型不包含大于零的字段, 则其大小为零 |
array | (元素值大小) * (数组长度) | 如果数组的元素类型大小为零, 则其大小为零 |
值拷贝代价
一般来说, 拷贝值的成本与值的大小成正比. 但是, 值的大小并不是计算值拷贝的唯一因素. 不同的 CPU 体系结构可能会针对具有特定大小的值专门优化值拷贝. 在实践中, 我们可以将大小小于四个本地字的值视为小值. 拷贝小值的成本很小.
除了大型结构和数组类型的值(对于标准的 Go 编译器), Go 中的大部分值都是小值.
为避免参数传递和 channel 值发送和接收操作中的大值复制成本, 我们应该尽量避免使用大型结构和数组类型作为函数和方法参数类型(包括方法接收器类型) 并避免使用大型结构和数组类型作为 channel 元素类型. 我们可以使用基类型为大型结构和数组类型的指针类型来应对这种情况.
另一方面, 我们还应该考虑在运行时使用太多指针造成的垃圾回收的负面影响. 因此, 是否应该使用大型结构体和数组类型或其相应的指针类型取决于特定的场景.
通常, 我们不应该使用基类型为 slice, map, channel map, function, 字符串和接口类型的指针类型. 这些假定的基本类型的值拷贝的成本很小.
如果元素类型是大值类型, 我们还应该尝试避免使用两次迭代变量形式来迭代数组和切片元素, 因为每个元素值都将被复制到迭代过程中的第二个迭代变量(译注: for range 中的 v).
以下是一个示例, 用于对 slice 元素迭代的不同方式进行基准测试.
package main |
在测试文件的目录中运行基准测试, 我们将得到类似于以下的结果:
Benchmark_Loop-4 500000 3228 ns/op |
我们可以发现, 双迭代变量形式的效率远低于其他两种形式.