← Назад к вопросам

Что происходит при попытке выделить один килобайт памяти?

1.2 Junior🔥 222 комментариев
#Основы Go#Производительность и оптимизация

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI7 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Общий процесс выделения памяти в Go

При попытке выделить один килобайт (1024 байта) памяти в Go запускается сложный многоуровневый процесс, который сильно отличается от простого вызова malloc в C. Давайте разберем его поэтапно.

1. Инициирование выделения памяти

Когда ваша программа Go вызывает make(), new() или создает сложную структуру данных, компилятор генерирует код, который в конечном счете вызывает функции рантайма для выделения памяти.

// Пример выделения ~1KB
data := make([]byte, 1024) // Выделение среза байтов размером 1KB

2. Работа аллокатора Go

Аллокатор Go использует многоуровневую стратегию для эффективного управления памятью:

Уровень 1: P-кэш (Per-Processor Cache)

Каждый логический процессор (P) в Go имеет локальный кэш (mcache) для малых объектов. Аллокатор сначала проверяет, можно ли удовлетворить запрос из этого кэша:

  • Для 1KB (1024 байт) объект классифицируется как малый объект (меньше 32KB)
  • Go использует систему размерных классов (size classes) - предопределенных размеров памяти
  • 1KB попадет в ближайший подходящий размерный класс (например, 1152 байта с учетом заголовков)

Уровень 2: Центральный кэш (Central Cache)

Если в P-кэше нет свободных слотов нужного размерного класса:

// Псевдокод логики аллокатора
if slotAvailableInMCache {
    allocateFromMCache()
} else {
    refillMCacheFromCentralCache() // Заполнение из mcentral
}

Уровень 3: Глобальная куча (Heap)

Если центральный кэш исчерпан, происходит выделение из глобальной кучи:

3. Выделение из глобальной кучи

Аренда памяти (span allocation):

  • Go управляет памятью блоками по 8KB (страницами)
  • Несколько страниц объединяются в span - непрерывную область памяти одного размерного класса
  • Для выделения 1KB аллокатор:
    1. Находит или создает span подходящего размерного класса
    2. Размечает span на слоты фиксированного размера
    3. Возвращает один свободный слот

4. Учет и управление памятью

Заголовки объектов: Каждому выделенному объекту добавляется служебная информация:

// Структура заголовка (упрощенно)
type heapHeader struct {
    size    uintptr  // Размер объекта
    spanPtr *mspan   // Ссылка на родительский span
    // Флаги сборки мусора и др.
}

Триггеры сборки мусора: Выделение памяти может активировать сборку мусора если:

  • Достигнут порог GOGC (по умолчанию 100%)
  • Произошло выделение большого объема памяти
  • Выполняется системный вызов runtime.GC()

5. Особенности для разных сценариев

Выделение на стеке vs куче

Компилятор Go использует escape analysis чтобы определить, можно ли выделить память на стеке:

func allocateOnStack() {
    data := [1024]byte{} // Вероятно останется на стеке
}

func allocateOnHeap() *[1024]byte {
    data := [1024]byte{} // Убегает (escape) - выделяется в куче
    return &data
}

Большие объекты (≥32KB)

Хотя 1KB - малый объект, для полноты картины:

  • Объекты ≥32KB выделяются напрямую из глобальной кучи
  • Не используют систему размерных классов
  • Управляются отдельно как большие объекты

6. Системный уровень

На самом низком уровне Go использует системные вызовы:

  • Linux: mmap() или brk() для расширения heap
  • Windows: VirtualAlloc()
  • Выделяется память большими блоками (обычно 1MB+), которые затем делятся

7. Синхронизация и параллелизм

Важные аспекты:

  • P-кэши уменьшают contention (состязание) между горутинами
  • Центральные кэши защищены мьютексами
  • Используются lock-free структуры где возможно
  • False sharing минимизирован через выравнивание

Практический пример

package main

func main() {
    // При компиляции это преобразуется в вызов runtime.mallocgc
    var kb [1024]byte
    
    // Под капотом происходит примерно:
    // 1. Проверка escape analysis
    // 2. Поиск в mcache текущего P
    // 3. При необходимости refill из mcentral
    // 4. Выделение с учетом size class
    // 5. Возврат указателя на память
}

Оптимизации и тонкости

  1. Предвыделение (Bump allocation): В пределах span используется быстрая линейная аллокация
  2. Пул потоков (Thread pooling): Работа с памятью привязана к P, не к системным потокам
  3. Локализация данных: Смежные объекты часто выделяются рядом для улучшения кэширования CPU
  4. Zero initialization: Вся новая память обнуляется (требование спецификации Go)

Производительность: Выделение 1KB в Go обычно занимает 10-50 наносекунд при наличии свободных слотов в P-кэше, но может замедлиться до микросекунд при необходимости сборки мусора или расширения heap.

Этот многоуровневый подход объясняет, почему Go демонстрирует отличную производительность при интенсивном выделении/освобождении мелких объектов - типичном сценарии для сетевых серверов и конкурентных приложений.

Что происходит при попытке выделить один килобайт памяти? | PrepBro