День 2: Структуры в памяти — Layout, Alignment, Padding
🎯 Темы дня
- Как структуры хранятся в памяти
- Alignment и padding — почему размер не всегда равен сумме полей
- Оптимизация порядка полей
- Инструменты анализа:
unsafe.Sizeof,structslop
📖 1. Layout структур
Как хранятся поля
Поля структуры хранятся последовательно в памяти в порядке объявления:
gotype User struct { Name string // 16 байт (pointer + length) Age int // 8 байт Active bool // 1 байт }
В памяти это выглядит так:
| Name.ptr (8) | Name.len (8) | Age (8) | Active (1) |
Но есть нюанс: Alignment
Компилятор выравнивает поля для оптимального доступа к памяти. CPU быстрее читает данные, когда они выровнены по границам, кратным их размеру.
Правила выравнивания (на 64-битных системах):
bool,int8,uint8— выравнивание 1 байтint16,uint16— выравнивание 2 байтаint32,uint32,float32— выравнивание 4 байтаint64,uint64,float64, pointers — выравнивание 8 байтstring, slices — выравнивание 8 байт (из-за указателя)
📖 2. Padding — скрытая цена
Что такое padding
Padding — это пустые байты между полями (или в конце структуры), добавленные для выравнивания.
Пример
gotype Bad struct { a bool // 1 байт b int64 // 8 байт c bool // 1 байт }
Как это выглядит в памяти:
| a (1) | padding (7) | b (8) | c (1) | padding (7) |
Размер: 1 + 7 + 8 + 1 + 7 = 24 байта
Почему так?
b(int64) требует выравнивания по 8 байт.- После
a(1 байт) нужно 7 байт padding, чтобыbначался с адреса, кратного 8. - После
c(1 байт) нужно 7 байт padding, чтобы размер структуры был кратен её максимальному выравниванию (8).
Оптимизация: правильный порядок полей
gotype Good struct { b int64 // 8 байт a bool // 1 байт c bool // 1 байт // padding: 6 байт }
Размер: 8 + 1 + 1 + 6 = 16 байт (вместо 24!)
Правило: Располагай поля от больших к маленьким.
На диаграмме те же два варианта по байтам: серым — padding. Множество полей одно и то же, меняется только порядок — и структура худеет с 24 до 16 байт.
📖 3. Практические примеры
Пример 1: Наивная структура
gotype Request struct { Active bool // 1 байт ID int64 // 8 байт Timestamp int64 // 8 байт Method string // 16 байт Retries int32 // 4 байта }
Размер: 1 + 7 (padding) + 8 + 8 + 16 + 4 + 4 (padding) = 48 байт
Пример 2: Оптимизированная структура
gotype Request struct { ID int64 // 8 байт Timestamp int64 // 8 байт Method string // 16 байт Retries int32 // 4 байта Active bool // 1 байт // padding: 3 байта }
Размер: 8 + 8 + 16 + 4 + 1 + 3 (padding) = 40 байт
Экономия: 8 байт на структуру (16%).
Пример 3: Экстремальная оптимизация
gotype Bad struct { a bool // 1 b int64 // 8 c bool // 1 d int32 // 4 e bool // 1 } // Размер: 32 байта type Good struct { b int64 // 8 d int32 // 4 a bool // 1 c bool // 1 e bool // 1 // padding: 1 } // Размер: 16 байт
Экономия: 16 байт (50%!).
📖 4. Инструменты анализа
unsafe.Sizeof
goimport "unsafe" type MyStruct struct { a bool b int64 c bool } func main() { var s MyStruct fmt.Println(unsafe.Sizeof(s)) // 24 }
structlayout (из go tools)
bashgo install golang.org/x/tools/go/analysis/passes/structlayout/cmd/structlayout@latest structlayout -json . MyStruct | structlayout-optimize
Вывод покажет оптимальный порядок полей.
structslop
bashgo install github.com/orijtech/structslop@latest structslop analyze ./...
Покажет, сколько памяти можно сэкономить, переупорядочив поля.
💻 Практика (1–2 часа)
Задание 1: Анализ размера
Для каждой структуры определи размер и объясни, почему именно так:
gotype A struct { a bool b int64 c bool } type B struct { a int32 b int32 c bool } type C struct { a string b int64 c bool }
Ответы:
- A: 24 байта (1 + 7 padding + 8 + 1 + 7 padding)
- B: 12 байт (4 + 4 + 1 + 3 padding)
- C: 32 байта (16 + 8 + 1 + 7 padding)
Задание 2: Оптимизация
Оптимизируй эти структуры:
gotype Request struct { Active bool ID int64 Timestamp int64 Method string Retries int32 } type Event struct { Type string ID int64 Payload []byte Sync bool }
Ответы:
gotype Request struct { ID int64 // 8 Timestamp int64 // 8 Method string // 16 Retries int32 // 4 Active bool // 1 // padding: 3 } // Размер: 40 байт type Event struct { ID int64 // 8 Payload []byte // 24 Type string // 16 Sync bool // 1 // padding: 7 } // Размер: 56 байт
Задание 3: Бенчмарк влияния padding
Напиши бенчмарк, сравнивающий производительность итерации по слайсу неоптимизированной и оптимизированной структур:
gofunc BenchmarkBadStruct(b *testing.B) { items := make([]Bad, 1000) for i := 0; i < b.N; i++ { for _, item := range items { _ = item.b } } } func BenchmarkGoodStruct(b *testing.B) { items := make([]Good, 1000) for i := 0; i < b.N; i++ { for _, item := range items { _ = item.b } } }
Что смотреть: Cache locality — оптимизированная структура лучше помещается в CPU cache.
🗣️ Каверзные вопросы
-
Почему размер структуры не всегда равен сумме размеров полей? Ответ: Из-за padding — пустых байт для выравнивания полей.
-
Как уменьшить размер структуры? Ответ: Переупорядочить поля от больших к маленьким.
-
Что такое alignment? Ответ: Требование, чтобы адрес переменной был кратным определённому числу (обычно размеру типа).
-
Почему оптимизация размера важна? Ответ: Меньше размер → лучше cache locality → быстрее итерация по массивам структур.
-
Что выведет
unsafe.Sizeofдляstruct{}? Ответ: 0. Пустая структура не занимает места. -
Можно ли использовать
struct{}для экономии памяти в map? Ответ: Да!map[string]struct{}занимает меньше памяти, чемmap[string]bool.
🧩 Проверь себя
✅ Чек-лист дня
- Понимаю, как поля структуры хранятся в памяти
- Знаю, что такое alignment и padding
- Могу рассчитать размер структуры вручную
- Умею оптимизировать порядок полей
- Запустил
unsafe.Sizeofи проверил свои расчёты - Понимаю, почему оптимизация размера важна для производительности