值复制成本
一个值的尺寸表示此值的在内存中占用多少个字节,它的间接部分(如果存在的话)对它的尺寸没有贡献。
在Go中,如果两个值的类型为同一种类的类型,并且它们的类型的种类不为字符串、接口、数组和结构体,则这两个值的尺寸总是相等的。
事实上,对于官方标准编译器来说,任意两个字符串值的尺寸总是相等的,即使它们的字符串类型并不是同一个类型。 同样地,任意两个接口值的尺寸也都是相等的。
目前(Go 1.17),至少对于官方标准编译器来说,任何一个特定类型的所有值的尺寸都是相同的。所以我们也常说一个值的尺寸为此值的类型的尺寸。
一个结构体类型的尺寸取决于它的各个字段的类型尺寸和这些字段的排列顺序。 为了程序执行性能,编译器需要保证某些类型的值在内存中存放时必须满足特定的要求。 地址对齐可能会造成相邻的两个字段之间在内存中被插入填充一些多余的字节。 所以,一个结构体类型的尺寸必定不小于(常常会大于)此结构体类型的各个字段的类型尺寸之和。
下表列出了各种种类的类型的尺寸(对标准编译器1.17来说)。 在此表中,一个word表示一个原生字。在32位系统架构中,一个word为4个字节;而在64位系统架构中,一个word为8个字节。
值复制成本
一般说来,复制一个值的成本正比于此值的尺寸。 但是,值尺寸并非是值复制成本的唯一决定因素。 不同的CPU型号和编译器版本可能会对某些特定尺寸的值的复制做了优化。
在实践中,我们可以将尺寸不大于4个原生字并且字段数不超过4个的结构体值看作是小尺寸值。复制小尺寸值的代价是比较小的。
为了防止在函数传参和通道操作中因为值复制代价太高而造成的性能损失,我们应该避免使用大尺寸的结构体和数组类型做为参数类型和通道的元素类型,应该在这些场合下使用基类型为这样的大尺寸类型的指针类型。 另一方面,我们也要考虑到太多的指针将会增加垃圾回收的压力。所以到底应该使用大尺寸类型还是以大尺寸类型为基类型的指针类型做为参数类型或通道的元素类型取决于具体的应用场景。
一般来说,在实践中,我们很少使用基类型为切片类型、映射类型、通道类型、函数类型、字符串类型和接口类型的指针类型,因为复制这些类型的值的代价很小。
如果一个数组或者切片的元素类型是一个大尺寸类型,我们应该避免在循环中使用双循环变量来遍历这样的数组或者切片类型的值中的元素。 因为,在遍历过程中,每个元素将被复制给第二个循环变量一次。
下面这个例子展示了三种遍历一个切片的方法的性能差异。
可以看出,使用双循环变量的方法的效率比另外两种方法的效率低得多。 但是请注意,某些编译器版本可能会做出一些特别的优化从而消除上面几种遍历方法的效率差异。 上面的基准测试结果基于Go标准编译器1.17版本。