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

Что происходит, когда в коде просим выделить 1Кб памяти?

1.7 Middle🔥 201 комментариев
#Операционные системы и Linux#Основы Go#Производительность и оптимизация

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

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

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

Введение в процесс выделения памяти

Когда в коде на Go мы просим выделить 1 КБ (1024 байта) памяти, за этой простой операцией скрывается сложная цепочка процессов, включающая взаимодействие с управлением памятью Go (memory management), сборщиком мусора (garbage collector), и в некоторых случаях — с операционной системой.

Основные этапы выделения памяти

1. Обработка запроса в рантайме Go

Когда выполняется операция, требующая выделения памяти (например, создание среза, структуры или использование make), управление передается аллокатору памяти Go:

// Примеры операций, требующих выделения ~1KB памяти:
data := make([]byte, 1024) // 1KB для байтового среза
type Record struct {
    ID   int64    // 8 байт
    Data [256]int // 1024 байта (256 * 4)
}
var rec Record // Выделение памяти под структуру

2. Работа с размерами объектов

Go классифицирует объекты по размеру:

  • Малые объекты (≤ 32KB): обрабатываются через mcache (локальный кэш потока)
  • Крупные объекты (> 32KB): выделяются непосредственно в куче (heap)

1KB — это малый объект, поэтому он будет обработан через механизм малых объектов.

3. Иерархия аллокаторов Go

Go использует многоуровневую систему аллокации:

Запрос 1KB → P (процессор) → mcache → mcentral → mheap → ОС

mcache (локальный кэш): Каждый поток (P) имеет собственный кэш для быстрого выделения без блокировок. Аллокатор ищет подходящий span (блок памяти фиксированного размера классов).

Классы размеров: Go использует около 70 фиксированных классов размеров. Для 1KB будет выбран ближайший подходящий класс (например, 1152 байта).

// Внутренняя структура mcache (упрощенно)
type mcache struct {
    tiny           uintptr
    alloc [numSpanClasses]*mspan // Список spans по классам
}

4. Процесс выделения шаг за шагом

  1. Проверка mcache: Аллокатор проверяет локальный кэш текущего P на наличие свободного места в соответствующем классе размера
  2. Если место есть: Память сразу выделяется из mcache (самый быстрый путь)
  3. Если места нет: Аллокатор обращается к mcentral (центральный список) за новым span
  4. Если в mcentral нет свободных span: Запрашивается новый span из mheap (главной кучи)
  5. Если в mheap недостаточно памяти: Выполняется системный вызов к ОС (например, mmap в Linux) для расширения виртуального адресного пространства

5. Взаимодействие с операционной системой

Изначально Go резервирует большой регион виртуальной памяти (arena), но физическая память выделяется лениво (on-demand) через механизм page faults:

// Пример, как выделение памяти может привести к page fault
func allocateMemory() {
    // Виртуальная память резервируется сразу
    // Физическая память выделяется при первой записи
    buf := make([]byte, 1024)
    buf[0] = 1 // Здесь может произойти page fault
}

6. Влияние сборщика мусора (GC)

Выделение памяти тесно связано с работой GC:

  • Триггер GC: Если объем выделенной памяти превышает пороговое значение, запускается сборка мусора
  • Write Barrier: При записи указателей в кучу устанавливается барьер для отслеживания изменений GC
  • Marking phase: GC помечает достижимые объекты, включая наш 1KB блок, если на него есть ссылки

7. Оптимизации для малых объектов

Для объектов размером 1KB применяются специальные оптимизации:

  • Tiny allocator: Очень маленькие объекты (< 16B) объединяются в блоки
  • Локальность ссылок: Смежные выделения стремятся разместить рядом для улучшения кэширования
  • Выравнивание: Память выравнивается по границам, соответствующим архитектуре (обычно 8 или 16 байт)

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

func allocateAndUse() {
    // Выделение 1KB памяти
    buffer := make([]byte, 1024)
    
    // В этот момент происходит:
    // 1. Проверка размера (1024 байта = малый объект)
    // 2. Поиск в mcache текущего P
    // 3. Если нет места — обращение к mcentral
    // 4. Инициализация памяти (возможно обнуление)
    
    // Использование памяти
    for i := range buffer {
        buffer[i] = byte(i % 256)
    }
    
    // Когда функция завершится и buffer станет недостижим:
    // Память будет освобождена при следующем цикле GC
}

Производительность и накладные расходы

  • Быстрые пути: Большинство выделений выполняются через mcache без блокировок
  • Накладные расходы: Каждый объект имеет заголовок (около 16 байт) для метаданных GC
  • Фрагментация: Go минимизирует фрагментацию через систему span и классы размеров
  • Память возвращается ОС: Только большими блоками и не всегда сразу (через madvise)

Заключение

Выделение 1KB памяти в Go — это высокооптимизированный процесс, использующий многоуровневую систему кэшей, пулов памяти по классам размеров и тесную интеграцию со сборщиком мусора. Хотя пользователь видит простую операцию, рантайм Go выполняет сложную работу по балансировке между скоростью выделения, эффективностью использования памяти и снижением нагрузки на сборщик мусора, что является одним из ключевых преимуществ языка для высоконагруженных приложений.

Что происходит, когда в коде просим выделить 1Кб памяти? | PrepBro