Пост

Оптимизация использования памяти в структурах Go

Go — язык системного уровня, и в нём память под структуры выделяется с учётом выравнивания (alignment). Понимание этого механизма позволяет сократить лишние затраты и сделать код эффективнее.


Лишние байты из-за выравнивания

Пример

1
2
3
4
5
6
7
8
type People struct {
    Active    bool
    Height    uint8
    Weight    uint8
    Score     int64
    FirstName string
    LastName  string
}

На первый взгляд структура выглядит компактной: несколько чисел, строка и флаг. Но Go добавляет паддинги (заполнители пустых байтов), чтобы данные выравнивались по границам, удобным для процессора. Из-за этого итоговый размер структуры оказывается больше, чем можно подумать.

Проверяем размер структуры

Go позволяет узнать, сколько байт реально занимает структура

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"unsafe"
)

type People struct {
	Active    bool
	Height    uint8
	Weight    uint8
	Score     int64
	FirstName string
	LastName  string
}

func main() {
	var p People
	fmt.Println("Size of People:", unsafe.Sizeof(p))
}

👉 На 64-битной системе такой код выведет 64 байта.

Почему так много?

  • Поле bool занимает 1 байт, но дальше остаётся 7 байт паддинга, чтобы Score (int64) начинался с адреса, кратного 8.

  • Поля строк (string) — это указатели и длина, им также нужно выравнивание.

  • В результате структура сильно раздувается.

Оптимизация порядка полей

Если поменять порядок так, чтобы “крупные” типы шли первыми, а “мелкие” в конце, лишние паддинги исчезнут.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"unsafe"
)

type People struct {
    Score     int64
    FirstName string
    LastName  string
    Active    bool
    Height    uint8
    Weight    uint8
}

func main() {
	var p People
	fmt.Println("Size of People:", unsafe.Sizeof(p))
}

👉 Результат: 48 байт.

Мы сэкономили 25% памяти всего лишь перестановкой полей.

Общие правила оптимизации структур

  1. Сначала большие поля (указатели, строки, слайсы, интерфейсы, int64, float64).

  2. Мелкие числа (uint8, bool, int16 и т.д.) группируйте в конце.

  3. Если есть массивы или вложенные структуры — учитывайте их выравнивание.

  4. Объединяйте мелкие флаги в битовые маски.

❗️ Частая ошибка — хранить десятки bool внутри структуры. Каждый bool занимает 1 байт (и может тянуть за собой паддинг). Вместо этого лучше использовать uint8, uint32 или uint64 и хранить все флаги в битовом виде.

1
2
3
4
5
6
7
8
9
type Flags struct {
    Bits uint64 // до 64 флагов
}

// установка бита
func (f *Flags) Set(pos uint) { f.Bits |= 1 << pos }

// проверка бита
func (f *Flags) Has(pos uint) bool { return f.Bits&(1<<pos) != 0 }

Такой подход позволяет сократить десятки байт на каждую запись в массиве структур.

  1. Выносите редко используемые поля в отдельную структуру (object splitting).
1
2
3
4
5
6
7
8
9
10
11
12
type User struct {
    // часто используемые поля
    ID   int64
    Name string
    // редко используемые
    Extra *UserExtra
}

type UserExtra struct {
    Bio   string
    Tags  []string
}

Если часть полей используется редко, нет смысла хранить их прямо в основной структуре — это раздувает размер каждого объекта. Лучше вынести их в отдельный объект по указателю.

Таблица размеров и выравнивания типов в Go (64-битные системы)

go_structure_memory_table

Вывод

В Go расположение полей внутри структуры напрямую влияет на её размер в памяти. Грамотная перестановка и использование битовых масок вместо множества bool позволяет экономить десятки мегабайт при работе с большими коллекциями данных.

  • До: 64 байта

  • После: 48 байт

А если вместо 10 bool использовать один uint16, можно сэкономить ещё больше.

Больше полезной информации в Telegram-канале