在Go編程中,值復(fù)制是很常見的操作。賦值、傳參和通道發(fā)送操作均涉及到值復(fù)制。 本篇文章將談?wù)劯鞣N不同種類的類型的Go值的復(fù)制成本。
一個值的尺寸表示此值的直接部分在內(nèi)存中占用多少個字節(jié),它的間接部分(如果存在的話)對它的尺寸沒有貢獻(xiàn)。
在Go中,如果兩個值的類型為同一種類的類型,并且它們的類型的種類不為字符串、接口、數(shù)組和結(jié)構(gòu)體,則這兩個值的尺寸總是相等的。
事實上,對于官方標(biāo)準(zhǔn)編譯器來說,任意兩個字符串值的尺寸總是相等的,即使它們的字符串類型并不是同一個類型。 同樣地,任意兩個接口值的尺寸也都是相等的。
目前(Go 1.19),至少對于官方標(biāo)準(zhǔn)編譯器來說,任何一個特定類型的所有值的尺寸都是相同的。所以我們也常說一個值的尺寸為此值的類型的尺寸。
一個數(shù)組類型的尺寸取決于它的元素類型的尺寸和它的長度。它的尺寸為它的元素類型的尺寸和它的長度的乘積。
一個結(jié)構(gòu)體類型的尺寸取決于它的各個字段的類型尺寸和這些字段的排列順序。 為了程序執(zhí)行性能,編譯器需要保證某些類型的值在內(nèi)存中存放時必須滿足特定的內(nèi)存地址對齊要求。 地址對齊可能會造成相鄰的兩個字段之間在內(nèi)存中被插入填充一些多余的字節(jié)。 所以,一個結(jié)構(gòu)體類型的尺寸必定不小于(常常會大于)此結(jié)構(gòu)體類型的各個字段的類型尺寸之和。
下表列出了各種種類的類型的尺寸(對標(biāo)準(zhǔn)編譯器1.19來說)。 在此表中,一個word表示一個原生字。在32位系統(tǒng)架構(gòu)中,一個word為4個字節(jié);而在64位系統(tǒng)架構(gòu)中,一個word為8個字節(jié)。
類型種類 | 值尺寸 | Go白皮書中的要求 |
---|---|---|
布爾 | 1 byte | 未做特別要求 |
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 | 架構(gòu)相關(guān),在32位系統(tǒng)架構(gòu)中為4個字節(jié),而在64位系統(tǒng)架構(gòu)中為8個字節(jié) |
uintptr | 1 word | 必須足夠存下任一個內(nèi)存地址 |
字符串 | 2 words | 未做特別要求 |
指針和非類型安全指針 | 1 word | 未做特別要求 |
切片 | 3 words | 未做特別要求 |
映射 | 1 word | 未做特別要求 |
通道 | 1 word | 未做特別要求 |
函數(shù) | 1 word | 未做特別要求 |
接口 | 2 words | 未做特別要求 |
結(jié)構(gòu)體 | 所有字段尺寸之和 + 所有填充的字節(jié)數(shù) | 一個不含任何尺寸大于零的字段的結(jié)構(gòu)體類型的尺寸為零 |
數(shù)組 | 元素類型的尺寸 * 長度 | 一個元素類型的尺寸為零的數(shù)組類型的尺寸為零 |
一般說來,復(fù)制一個值的成本正比于此值的尺寸。 但是,值尺寸并非是值復(fù)制成本的唯一決定因素。 不同的CPU型號和編譯器版本可能會對某些特定尺寸的值的復(fù)制做了優(yōu)化。
在實踐中,我們可以將尺寸不大于4個原生字并且字段數(shù)不超過4個的結(jié)構(gòu)體值看作是小尺寸值。復(fù)制小尺寸值的代價是比較小的。
對于標(biāo)準(zhǔn)編譯器,除了大尺寸的結(jié)構(gòu)體和數(shù)組類型,其它類型均為小尺寸類型。
為了防止在函數(shù)傳參和通道操作中因為值復(fù)制代價太高而造成的性能損失,我們應(yīng)該避免使用大尺寸的結(jié)構(gòu)體和數(shù)組類型做為參數(shù)類型和通道的元素類型,應(yīng)該在這些場合下使用基類型為這樣的大尺寸類型的指針類型。 另一方面,我們也要考慮到太多的指針將會增加垃圾回收的壓力。所以到底應(yīng)該使用大尺寸類型還是以大尺寸類型為基類型的指針類型做為參數(shù)類型或通道的元素類型取決于具體的應(yīng)用場景。
一般來說,在實踐中,我們很少使用基類型為切片類型、映射類型、通道類型、函數(shù)類型、字符串類型和接口類型的指針類型,因為復(fù)制這些類型的值的代價很小。
如果一個數(shù)組或者切片的元素類型是一個大尺寸類型,我們應(yīng)該避免在for-range
循環(huán)中使用雙循環(huán)變量來遍歷這樣的數(shù)組或者切片類型的值中的元素。 因為,在遍歷過程中,每個元素將被復(fù)制給第二個循環(huán)變量一次。
下面這個例子展示了三種遍歷一個切片的方法的性能差異。
package main
import "testing"
type S [12]int64
var sX = make([]S, 1000)
var sY = make([]S, 1000)
var sZ = 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][0]
}
}
}
func Benchmark_Range_OneIterVar(b *testing.B) {
for i := 0; i < b.N; i++ {
sumY = 0
for j := range sY {
sumY += sY[j][0]
}
}
}
func Benchmark_Range_TwoIterVar(b *testing.B) {
for i := 0; i < b.N; i++ {
sumZ = 0
for _, v := range sZ {
sumZ += v[0]
}
}
}
運行此基準(zhǔn)測試,我們將得到下面的結(jié)果:
Benchmark_Loop-4 424342 2708 ns/op
Benchmark_Range_OneIterVar-4 407905 2808 ns/op
Benchmark_Range_TwoIterVar-4 214860 5222 ns/op
可以看出,使用雙循環(huán)變量的方法的效率比另外兩種方法的效率低得多。 但是請注意,某些編譯器版本可能會做出一些特別的優(yōu)化從而消除上面幾種遍歷方法的效率差異。 上面的基準(zhǔn)測試結(jié)果基于Go標(biāo)準(zhǔn)編譯器1.19版本。
更多建議: