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 ByGo 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 ofpadding bytes | astruct type has size zero if it contains no fields that have a size greater than zero |
array | (element value size) * (array length) | anarray 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-45000003228 ns/op Benchmark_Range_OneIterVar-45000003203 ns/op Benchmark_Range_TwoIterVars-42000006616 ns/op
我們可以發現,兩次迭代變數形式的效率遠低於其他兩種形式。