株式会社Schooの @hiroto_0411です!
Goのメモリアライメントと構造体のフィールドを定義する順番によるメモリ使用量の変化について調査してみました!
簡単にまとめると
- CPUが効率的に処理できるよう、メモリアライメントにより変数のアドレスは特定の倍数になるように調整される
- 構造体のフィールドを定義する順番によって、メモリ使用量は変化する
型それぞれのサイズ
func main() {
var num1 int64
fmt.Printf("Size of int64: %d bytes\n", unsafe.Sizeof(num1))
var num2 int32
fmt.Printf("Size of int32: %d bytes\n", unsafe.Sizeof(num2))
var num3 float64
fmt.Printf("Size of float64: %d bytes\n", unsafe.Sizeof(num3))
var num4 float32
fmt.Printf("Size of float32: %d bytes\n", unsafe.Sizeof(num4))
var bool bool
fmt.Printf("Size of bool: %d bytes\n", unsafe.Sizeof(bool))
var str string
fmt.Printf("Size of string: %d bytes\n", unsafe.Sizeof(str))
var slice []int
fmt.Printf("Size of slice: %d bytes\n", unsafe.Sizeof(slice))
}
Size of int64: 8 bytes
Size of int32: 4 bytes
Size of float64: 8 bytes
Size of float32: 4 bytes
Size of bool: 1 bytes
Size of string: 16 bytes
Size of slice: 24 bytes
変数とアドレス
64 bitシステムのCPUは8 bytesずつ処理していく。
CPUが効率的に処理できるよう、メモリアライメントにより変数のアドレスは特定の倍数になるように調整される。
int32は4 bytesのためアドレスが4の倍数になるように配置される。
この図のように4の倍数以外の箇所に配置すると、CPUが1サイクルで処理でき無くなる可能性があり効率が悪くなってしまう。
Goのコードで確認してみる
func main() {
var num1 int64
fmt.Printf("Memory address of num1: %p\n", &num1)
var num2 int32
fmt.Printf("Memory address of num2: %p\n", &num2)
}
出力
Memory address of num1: 0x14000120018
Memory address of num2: 0x14000120030
0x14000120018(16進数)→ 1374390714392(10進数) 8 bytes必要なint64はアドレスが8の倍数になるところへ配置される。
0x14000120030(16進数)→ 1374390714416(10進数) 4 bytes必要なint32はアドレスが4の倍数になるところへ配置される。
メモリアライメント
CPUが効率的にアクセスできるよう変数のアドレスを調整すること。
参考
構造体とメモリアライメント
type User struct {
ID int32 // 4 bytes
name string // 16 bytes
isAdmin bool // 1 byte
}
func main() {
user := User{ID: 1, name: "John", isAdmin: true}
fmt.Println(unsafe.Sizeof(user))
fmt.Printf("Memory address of ID: %p\n", &user.ID)
fmt.Printf("Memory address of name: %p\n", &user.name)
fmt.Printf("Memory address of isAdmin: %p\n", &user.isAdmin)
}
出力
32
Memory address of ID: 0x14000128000
Memory address of name: 0x14000128008
Memory address of isAdmin: 0x14000128018
0x14000128000 → 1374390747136 (10進数)
0x14000128008 → 1374390747144 (10進数)
0x14000128008 → 1374390747160 (10進数)
計算上だと構造体のサイズは4+16+1=21 bytesのはずだが、実際は32 bytesとなっている。これはメモリアライメントによって変数の配置される場所が調整されているためである。
イメージ
青色がデータを保持している箇所、赤色が調整するために加えられたpadding。
- IDはint32なので、4 bytes使用する
- nameはstringなので16bytes(8 bytes*2)使用するため、8の倍数となるメモリに配置する必要があり、IDの後に4 bytesのメモリがpaddingとして追加される
- isAdminはboolなので1 byteだけ使用するため、どこにでも配置できる
構造体のフィールド順番を変更してみる
type User struct {
name string // 16 bytes
ID int32 // 4 bytes
isAdmin bool // 1 byte
}
func main() {
user := User{ID: 1, name: "John", isAdmin: true}
fmt.Println(unsafe.Sizeof(user))
fmt.Printf("Memory address of name: %p\n", &user.name)
fmt.Printf("Memory address of ID: %p\n", &user.ID)
fmt.Printf("Memory address of isAdmin: %p\n", &user.isAdmin)
}
出力
24
Memory address of name: 0x14000128000
Memory address of ID: 0x14000128010
Memory address of isAdmin: 0x14000128014
0x14000128000 → 1374390747136 (10進数)
0x14000128010 → 1374390747152 (10進数)
0x14000128014 → 1374390747156 (10進数)
サイズが先ほどは32 bytesだったが、24 bytesとなっておりメモリを効率的に使うことができている。
イメージ
- nameはstringなので16 bytes(8 bytes*2)使用する
- IDはint32なので、4 bytes使用する
- isAdminはboolなので1 byte使用するため、IDの後に続けて配置することができる
このように、構造体のフィールドの順番を変えることでメモリ使用量を最適化することができる。
参考
Schooでは一緒に働く仲間を募集しています!