概要
Goにおけるstructのメモリ構造を知ることでフィールド順序に対する意識が変わったり、なぜunsafe.Sizeof(string)
が16bytes
でunsafe.Sizeof(slice)
が24bytes
になるかが理解できます。
環境
各型のメモリ割り当て
unsafe.Sizeof()を使うとその変数がどれくらいメモリを割り振るかが分かります。
※変数の分確保するメモリであり、参照先のメモリは含みません
型 | unsafe.Sizeof() |
---|---|
bool | 1 |
int32 | 4 |
int | 8 |
float64 | 8 |
string | 16 |
[]T | 24 |
structのフィールドにそれぞれの型を付けると、その分メモリが割り振られます
structのメモリ割り当て
例えばbool, float64, int32
のフィールドがあった場合、単純に足し算すると
となります。
では確かめてみましょう。
type myStruct struct { a bool // 1 byte b float64 // 8 bytes c int32 // 4 bytes } func main() { fmt.Println(unsafe.Sizeof(myStruct{})) // 24 bytes }
なんと24 bytes
と表示されました。どういうことでしょうか。
実際のメモリ割り当て
64 bitのシステムではメモリは8 Bytesの連続したパケットを割り当てます。
なので1 byte埋まってから8 bytesのフィールドが必要になっても割り当てられず、新しいパケットを使います。
そのため先程のstructは以下のようにメモリを割り当てます。
その結果
と表示されます。
フィールドの順番によってstructに使用するメモリは変わる
先程のロジックから考えると、フィールドの順番をうまく考えれば割り当てるメモリを減らすことができます。
type optimized struct { b float64 // 8 bytes a bool // 1 byte c int32 // 4 bytes } func main() { fmt.Println(unsafe.Sizeof(optimized{})) // 16 bytes }
このような感じです。16bytesに減りました。
実際のメモリ割り当て
このstructの場合、割り当ては以下のようにされています。
なので
となるわけです。
注意として4 bytesのint32は4 bytes毎の位置に割り当てられるので、boolの1 byteの直後になるわけではありません。
つまり以下のイメージは間違っています。
なのでboolを後ろに追加したとしても、新たに8 bytes割り当てられてしまいます。
type optimized struct { b float64 // 8 bytes a bool // 1 byte c int32 // 4 bytes d bool // 1 byte } func main() { fmt.Println(unsafe.Sizeof(optimized{})) // 24 bytes }
stringやslice
stringやsliceのunsafe.Sizeof()をしてみると16 bytesと24 bytesと出ます。
func main() { var ( str string si []int ss []string ) fmt.Println(unsafe.Sizeof(str)) // 16 bytes fmt.Println(unsafe.Sizeof(si)) // 24 bytes fmt.Println(unsafe.Sizeof(ss)) // 24 bytes }
上のコードを見て分かるように、sliceは型によらず24 bytesとなります。
これはなぜでしょうか。
stringのメモリ割り当て
実はstringの内部構造は以下のようになっています。
type StringHeader struct { Data uintptr // 8 bytes Len int // 8 bytes }
https://golang.org/pkg/reflect/#StringHeader
Dataは実際のデータを参照するためのポインタとしてのフィールドです。
この構造になっているため
と16 bytesになるわけです。
sliceのメモリ割り当て
sliceの内部構造は以下のようになっています。
type SliceHeader struct { Data uintptr // 8 bytes Len int // 8 bytes Cap int // 8 bytes }
https://golang.org/pkg/reflect/#SliceHeader
以前↓でも説明した構造ですね。
このためどの型であってもsliceは
の24 bytesと表示されるわけです。
その他
structのアドレスと最初のフィールドのアドレスは一致する
次のようなコードを用意してみます。
type Person struct { Name string Age int } func main() { x := Person{"alice", 20} fmt.Printf("x address:\t%p\n", &x) fmt.Printf("x.Name address:\t%p\n", &x.Name) fmt.Printf("x.Age address:\t%p\n", &x.Age) }
結果は以下で
x address: 0xc00000c030 x.Name address: 0xc00000c030 x.Age address: 0xc00000c040
structのアドレスと、最初のフィールドName
のアドレスは一致しています。
次のフィールドAge
は、Name
がstringなので16bytes後の0xc00000c030
→0xc00000c040
になっています。
まとめ
各型におけるメモリの割り当てと、structのメモリ割り当ての仕組みを説明しました。
またフィールドの順序によって割り当てる量が変わることも理解できました。
しかしながら実際の開発ではフィールドの順序はそのオブジェクトが持つ責務や可読性を優先した方が良いです。
特定のstructの巨大sliceがあり、そのメモリ使用量が高くボトルネックになっているという場合は改善の選択肢の1つとして考えると良いでしょう。