День 2: Структуры в памяти — Layout, Alignment, Padding

🎯 Темы дня

  1. Как структуры хранятся в памяти
  2. Alignment и padding — почему размер не всегда равен сумме полей
  3. Оптимизация порядка полей
  4. Инструменты анализа: unsafe.Sizeof, structslop

📖 1. Layout структур

Как хранятся поля

Поля структуры хранятся последовательно в памяти в порядке объявления:

go
type 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 — это пустые байты между полями (или в конце структуры), добавленные для выравнивания.

Пример

go
type 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).

Оптимизация: правильный порядок полей

go
type Good struct {
    b int64  // 8 байт
    a bool   // 1 байт
    c bool   // 1 байт
    // padding: 6 байт
}

Размер: 8 + 1 + 1 + 6 = 16 байт (вместо 24!)

Правило: Располагай поля от больших к маленьким.

Раскладка одной структуры: как есть и после перестановки полей

На диаграмме те же два варианта по байтам: серым — padding. Множество полей одно и то же, меняется только порядок — и структура худеет с 24 до 16 байт.


📖 3. Практические примеры

Пример 1: Наивная структура

go
type 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: Оптимизированная структура

go
type 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: Экстремальная оптимизация

go
type 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

go
import "unsafe"

type MyStruct struct {
    a bool
    b int64
    c bool
}

func main() {
    var s MyStruct
    fmt.Println(unsafe.Sizeof(s)) // 24
}

structlayout (из go tools)

bash
go install golang.org/x/tools/go/analysis/passes/structlayout/cmd/structlayout@latest

structlayout -json . MyStruct | structlayout-optimize

Вывод покажет оптимальный порядок полей.

structslop

bash
go install github.com/orijtech/structslop@latest

structslop analyze ./...

Покажет, сколько памяти можно сэкономить, переупорядочив поля.


💻 Практика (1–2 часа)

Задание 1: Анализ размера

Для каждой структуры определи размер и объясни, почему именно так:

go
type 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: Оптимизация

Оптимизируй эти структуры:

go
type Request struct {
    Active    bool
    ID        int64
    Timestamp int64
    Method    string
    Retries   int32
}

type Event struct {
    Type    string
    ID      int64
    Payload []byte
    Sync    bool
}

Ответы:

go
type 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

Напиши бенчмарк, сравнивающий производительность итерации по слайсу неоптимизированной и оптимизированной структур:

go
func 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.


🗣️ Каверзные вопросы

  1. Почему размер структуры не всегда равен сумме размеров полей? Ответ: Из-за padding — пустых байт для выравнивания полей.

  2. Как уменьшить размер структуры? Ответ: Переупорядочить поля от больших к маленьким.

  3. Что такое alignment? Ответ: Требование, чтобы адрес переменной был кратным определённому числу (обычно размеру типа).

  4. Почему оптимизация размера важна? Ответ: Меньше размер → лучше cache locality → быстрее итерация по массивам структур.

  5. Что выведет unsafe.Sizeof для struct{}? Ответ: 0. Пустая структура не занимает места.

  6. Можно ли использовать struct{} для экономии памяти в map? Ответ: Да! map[string]struct{} занимает меньше памяти, чем map[string]bool.


🧩 Проверь себя

1. Что выведет fmt.Println(unsafe.Sizeof(struct{}{}))?
2. Выравнивание каких типов на 64-битной системе равно 8 байтам? — выберите все подходящие
3. Как располагать поля структуры, чтобы минимизировать padding?
4. Какой размер у структуры struct { a bool; b int64; c bool } на 64-битной системе?
5. Почему unsafe.Sizeof для структуры может быть больше суммы размеров её полей?
6. Какая структура экономнее для множества (set) строк?

✅ Чек-лист дня

  • Понимаю, как поля структуры хранятся в памяти
  • Знаю, что такое alignment и padding
  • Могу рассчитать размер структуры вручную
  • Умею оптимизировать порядок полей
  • Запустил unsafe.Sizeof и проверил свои расчёты
  • Понимаю, почему оптимизация размера важна для производительности