Carpe Diem

備忘録

structのメモリ割り当て

概要

Goにおけるstructのメモリ構造を知ることでフィールド順序に対する意識が変わったり、なぜunsafe.Sizeof(string)16bytesunsafe.Sizeof(slice)24bytesになるかが理解できます。

環境

各型のメモリ割り当て

unsafe.Sizeof()を使うとその変数がどれくらいメモリを割り振るかが分かります。
※変数の分確保するメモリであり、参照先のメモリは含みません

unsafe.Sizeof()
bool 1
int32 4
int 8
float64 8
string 16
[]T 24

The Go Playground

structのフィールドにそれぞれの型を付けると、その分メモリが割り振られます

structのメモリ割り当て

例えばbool, float64, int32のフィールドがあった場合、単純に足し算すると

1 + 8 + 4 = 13 bytes

となります。

では確かめてみましょう。

type myStruct struct {
    a bool    // 1 byte
    b float64 // 8 bytes
    c int32   // 4 bytes
}

func main() {
    fmt.Println(unsafe.Sizeof(myStruct{}))  // 24 bytes
}

The Go Playground

なんと24 bytesと表示されました。どういうことでしょうか。

実際のメモリ割り当て

64 bitのシステムではメモリは8 Bytesの連続したパケットを割り当てます。
なので1 byte埋まってから8 bytesのフィールドが必要になっても割り当てられず、新しいパケットを使います。
そのため先程のstructは以下のようにメモリを割り当てます。

f:id:quoll00:20201205173457p:plain

その結果

8 + 8 + 8 = 24 bytes

と表示されます。

フィールドの順番によって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
}

The Go Playground

このような感じです。16bytesに減りました。

実際のメモリ割り当て

このstructの場合、割り当ては以下のようにされています。

f:id:quoll00:20201205173920p:plain

なので

 8 + 8 = 16 bytes

となるわけです。

注意として4 bytesのint32は4 bytes毎の位置に割り当てられるので、boolの1 byteの直後になるわけではありません
つまり以下のイメージは間違っています。

f:id:quoll00:20201205175026p:plain

なので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
}

The Go Playground

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
}

The Go Playground

上のコードを見て分かるように、sliceは型によらず24 bytesとなります。
これはなぜでしょうか。

stringのメモリ割り当て

実はstringの内部構造は以下のようになっています。

type StringHeader struct {
    Data uintptr // 8 bytes
    Len  int     // 8 bytes
}

https://golang.org/pkg/reflect/#StringHeader

Dataは実際のデータを参照するためのポインタとしてのフィールドです。

この構造になっているため

 8 + 8 = 16 bytes

と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

以前↓でも説明した構造ですね。

christina04.hatenablog.com

このためどの型であってもsliceは

 8 + 8 + 8 = 24 bytes

の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)
}

The Go Playground

結果は以下で

x address:   0xc00000c030
x.Name address: 0xc00000c030
x.Age address:  0xc00000c040

structのアドレスと、最初のフィールドNameのアドレスは一致しています。
次のフィールドAgeは、Nameがstringなので16bytes後の0xc00000c0300xc00000c040になっています。

まとめ

各型におけるメモリの割り当てと、structのメモリ割り当ての仕組みを説明しました。
またフィールドの順序によって割り当てる量が変わることも理解できました。
しかしながら実際の開発ではフィールドの順序はそのオブジェクトが持つ責務や可読性を優先した方が良いです。

特定のstructの巨大sliceがあり、そのメモリ使用量が高くボトルネックになっているという場合は改善の選択肢の1つとして考えると良いでしょう。

参考