Go言語でメモリ上の大きさや配置を調べる

golangで構造体を定義した場合、 メモリ上にどのように配置されるのでしょうか。

通常意識する必要はありませんが、32bitと64bitで挙動がおかしい場合など、
ごく希に調べる必要に迫られる場合があります。
そのような場合、各要素のサイズや、構造体先頭からのオフセットを調べることで、
メモリ上に構造体がどう置かれるかを調べることができます。

C言語でのsizeofやoffsetofに対応する物が、golangのunsafeパッケージに用意されているため、
これを利用することで構造体の様子を調べることができます。
https://golang.org/pkg/unsafe/

今回は以下のようなテスト構造体を使い、メモリ上にどのように置かれるかを調べました。
テスト環境はwindows7(32bit)とmac(64bit)になります。

type A struct {
  flag bool
  num int64
  ptr *int64
  mini int32
  str string
  nums []int64
  nums5 [5]int64
  strs []string
}

要素のサイズ

unsafe.Sizeof関数は、引数の要素のサイズを調べ、バイト数をint型で返してくれます。

a := A{}
log.Println(unsafe.Sizeof(a)) // 92 (136) 括弧外は32bit環境、括弧内は64bit
log.Println(unsafe.Sizeof(a.flag)) // 1
log.Println(unsafe.Sizeof(a.num)) // 8
log.Println(unsafe.Sizeof(a.ptr)) // 4 (8)
log.Println(unsafe.Sizeof(a.mini)) // 4
log.Println(unsafe.Sizeof(a.str)) // 8 (16)
log.Println(unsafe.Sizeof(a.nums)) // 12 (24)
log.Println(unsafe.Sizeof(a.nums5)) // 40
log.Println(unsafe.Sizeof(a.strs)) // 12 (24)

a.nums = append(a.nums, 42)
a.strs = append(a.strs, "デーデッデー")
log.Println(unsafe.Sizeof(a.nums)) // 12 (24)
log.Println(unsafe.Sizeof(a.strs)) // 12 (24)

log.Println(unsafe.Sizeof(&a)) // 4 (8)

この関数は指定した要素のサイズを返します。 そのため、32bit環境ではint64のポインタであるa.ptrは、
ポインタのサイズである4byte(=32bit)を返します。

また、配列の場合は配列全体のサイズ(a.nums5 = 64 * 5)を返しますが、
スライスの場合はスライス自身の値(12 or 24byte)を返し、スライス全体の値は返しません。
そのため、要素を追加してもスライス自身の大きさは変化しません。

先頭からのオフセット

上記の例でbool値のa.flagが1byteかつ、ほかの要素がすべて偶数なのにもかかわらず、
構造体全体の大きさが92byteの偶数になっています。
このことから、golangの構造体はパディングされる場合があることがわかります。
そのため、要素のサイズを足していったところが、実際にメモリ上に置かれる場所とは限りません。

構造体先頭からのオフセットが何byteかをint型で返す、unsafe.Offsetofを使うことで、
実際にどの位置に配置されているかを確認できます。

log.Println(unsafe.Offsetof(a.flag)) // 0
log.Println(unsafe.Offsetof(a.num)) // 4 (8)
log.Println(unsafe.Offsetof(a.ptr)) // 12 (16)
log.Println(unsafe.Offsetof(a.mini)) // 16 (24)
log.Println(unsafe.Offsetof(a.str)) // 20 (32)
log.Println(unsafe.Offsetof(a.nums)) // 28 (48)
log.Println(unsafe.Offsetof(a.nums5)) // 40 (72)
log.Println(unsafe.Offsetof(a.strs)) // 80 (112)

32bit環境ではa.flagの1byteの後に3byteパディングがされ、
a.numが先頭から4byte(=32bit)の位置から始まっています。

64bit環境ではa.flagの後ろに7byteパディングされ、
a.numが先頭から8byte(=64bit)の位置から始まっています。
また、int32のa.miniも4byteパディングされ、a.strが32byte目から始まるようになっています。

このように、32bit環境では32bitに、64bit環境では64bitの倍数から始まるように、
コンパイラが構造体にパディングをするようです。

構造体内の構造体の場合

以下のように構造体に実態を持つ場合、宣言したとおりに配置されます。
そのためAの実態であるaの前後で、構造体のサイズ分だけオフセットが移動しています。

type B struct {
  bflag bool
  intnum int64
  a A
  bflag2 bool
  a2 *A
  bflag3 bool
  A
  blfag4 bool
}
b := B{
  a2: &A{},
}
log.Println(unsafe.Offsetof(b.bflag)) // 0
log.Println(unsafe.Offsetof(b.intnum)) // 4
log.Println("b.a")
log.Println(unsafe.Offsetof(b.a.flag)) // 0
log.Println(unsafe.Offsetof(b.a.strs)) // 80
log.Println("b.flag2")
log.Println(unsafe.Offsetof(b.bflag2)) // 104 (4 + 8 + 92)
log.Println("b.a2") // a2 is pointer
log.Println(unsafe.Offsetof(b.a2.flag)) // 0
log.Println(unsafe.Offsetof(b.a2.strs)) // 80 (112)
log.Println("b.bflag3")
log.Println(unsafe.Offsetof(b.bflag3)) // 112 (104 + 8)
log.Println("b.flag")
log.Println(unsafe.Offsetof(b.flag)) // 116
log.Println(unsafe.Offsetof(b.num)) // 120
log.Println(unsafe.Offsetof(b.ptr)) // 128
log.Println(unsafe.Offsetof(b.mini)) // 132
log.Println(unsafe.Offsetof(b.strs)) // 196
log.Println("b.bflag4")
log.Println(unsafe.Offsetof(b.blfag4)) // 208 (112 + 92)

unsafe.Offsetofは対象の要素を持つ構造体の先頭からのオフセットを指すため、
構造体Bの中にあるAでも、Bからのオフセットではなく、Aの先頭からのオフセットを出力しています。

また、ポインタとして持つ場合、実態は別のところにあるため、a2のサイズはポインタ分のみになります。

構造体を埋め込んだ場合、それらの値がそのままそこに置かれたかのように置かれます。
また、埋め込んだ構造体の値として扱われるらしく、
実態として持っているときと違い、構造体Aの要素でもBの先頭からのオフセットを出力しています。

ソースコード

package main

import (
  "log"
  "unsafe"
)

type A struct {
  flag bool
  num int64
  ptr *int64
  mini int32
  str string
  nums []int64
  nums5 [5]int64
  strs []string
}

type B struct {
  bflag bool
  intnum int64
  a A
  bflag2 bool
  a2 *A
  bflag3 bool
  A
  blfag4 bool
}

func main() {
  log.Println("sizeof")
  a := A{}
  log.Println(unsafe.Sizeof(a)) // 92 (136) 括弧内は64bit
  log.Println(unsafe.Sizeof(a.flag)) // 1
  log.Println(unsafe.Sizeof(a.num)) // 8
  log.Println(unsafe.Sizeof(a.ptr)) // 4 (8)
  log.Println(unsafe.Sizeof(a.mini)) // 4
  log.Println(unsafe.Sizeof(a.str)) // 8 (16)
  log.Println(unsafe.Sizeof(a.nums)) // 12 (24)
  log.Println(unsafe.Sizeof(a.nums5)) // 40
  log.Println(unsafe.Sizeof(a.strs)) // 12 (24)

  a.nums = append(a.nums, 42)
  a.strs = append(a.strs, "デーデッデー")
  log.Println(unsafe.Sizeof(a.nums)) // 12 (24)
  log.Println(unsafe.Sizeof(a.strs)) // 12 (24)

  log.Println(unsafe.Sizeof(&a)) // 4 (8)

  log.Println("offest")
  log.Println(unsafe.Offsetof(a.flag)) // 0
  log.Println(unsafe.Offsetof(a.num)) // 4 (8)
  log.Println(unsafe.Offsetof(a.ptr)) // 12 (16)
  log.Println(unsafe.Offsetof(a.mini)) // 16 (24)
  log.Println(unsafe.Offsetof(a.str)) // 20 (32)
  log.Println(unsafe.Offsetof(a.nums)) // 28 (48)
  log.Println(unsafe.Offsetof(a.nums5)) // 40 (72)
  log.Println(unsafe.Offsetof(a.strs)) // 80 (112)



  log.Println("b")
  b := B{
    a2: &A{},
  }
  log.Println(unsafe.Offsetof(b.bflag)) // 0
  log.Println(unsafe.Offsetof(b.intnum)) // 4
  log.Println("b.a")
  log.Println(unsafe.Offsetof(b.a.flag)) // 0
  log.Println(unsafe.Offsetof(b.a.strs)) // 80
  log.Println("b.flag2")
  log.Println(unsafe.Offsetof(b.bflag2)) // 104 (4 + 8 + 92)
  log.Println("b.a2") // a2 is pointer
  log.Println(unsafe.Offsetof(b.a2.flag)) // 0
  log.Println(unsafe.Offsetof(b.a2.strs)) // 80 (112)
  log.Println("b.bflag3")
  log.Println(unsafe.Offsetof(b.bflag3)) // 112 (104 + 8)
  log.Println("b.flag")
  log.Println(unsafe.Offsetof(b.flag)) // 116
  log.Println(unsafe.Offsetof(b.num)) // 120
  log.Println(unsafe.Offsetof(b.ptr)) // 128
  log.Println(unsafe.Offsetof(b.mini)) // 132
  log.Println(unsafe.Offsetof(b.strs)) // 196
  log.Println("b.bflag4")
  log.Println(unsafe.Offsetof(b.blfag4)) // 208 (112 + 92)
}