Как работает Memory Allocator?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Как работает Memory Allocator в Go
Memory Allocator (аллокатор памяти) в Go — это сложный механизм управления динамической памятью, спроектированный для эффективного выделения и освобождения памяти в многопоточных средах. Он сочетает в себе концепции из TCMalloc (Thread-Caching Malloc) от Google и собственные оптимизации Go.
Основные компоненты аллокатора
Аллокатор оперирует следующими структурными элементами:
-
Классы размеров (Size Classes): Go не выделяет память произвольного размера. Вместо этого определены фиксированные классы размеров (например, 8, 16, 32, 48, ..., 32768 байт). Запрос округляется вверх до ближайшего класса. Это уменьшает фрагментацию и ускоряет поиск подходящего блока.
// Пример: выделение памяти под структуру будет округлено type Small struct { a, b int32 } // Размер = 8 байт // Выделится блок из класса size=8, даже если реально нужно 8 байт. -
Спаны (mspan): Основная единица управления. Это непрерывный регион памяти (обычно 8 КБ или больше), разделённый на блоки одного класса размера. Спан служит контейнером для объектов.
-
Кэши (mcache): Каждая горутина (G) имеет локальный кэш (mcache), содержащий список свободных спанов для каждого класса размеров. Выделение памяти из mcache не требует блокировок, что критически важно для производительности.
// Псевдо-структура mcache (упрощённо) type mcache struct { tiny uintptr // Кэш для мелких (<16B) внеклассовых объектов alloc [numSizeClasses]*mspan // Спаны для каждого класса } -
Центральные кэши (mcentral): Обслуживают каждый класс размеров глобально для всей программы (P — процессорная нить). Когда mcache исчерпывает свободные блоки в своём спане, он обращается к соответствующему mcentral за новым свободным спаном.
-
Куча (mheap): Глобальная куча, управляющая всей памятью процесса. Она запрашивает большие регионы памяти у ОС (через
mmapилиVirtualAlloc), разбивает их на страницы и организует в спаны. Когда mcentral исчерпывает спаны, он обращается к mheap.
Принцип работы выделения памяти
Рассмотрим путь типичного вызова make или new:
obj := make([]byte, 100) // Запрос 100 байт
- Определение класса размера: Размер 100 байт округляется до класса 112 байт.
- Поиск в локальном кэше (mcache):
* Аллокатор проверяет горутино-локальный mcache (привязанный к текущему P) на наличие свободного блока в спане для класса 112.
* Если блок найден — выделение мгновенное, без блокировок.
- Обращение к центральному кэшу (mcentral):
* Если в локальном спане нет свободных блоков, аллокатор обращается к mcentral для данного класса.
* mcentral возвращает частично заполненный или новый спан. Эта операция защищена **мьютексом**, так как mcentral общий для всех P.
- Запрос у глобальной кучи (mheap):
* Если mcentral пуст, он запрашивает новый спан у mheap.
* mheap может выделить новый большой регион памяти у ОС, разметить его на спаны и добавить в mcentral.
- Особые случаи: Для очень больших объектов (>32 КБ) аллокатор выделяет память напрямую из mheap, минуя классы размеров и кэши (этот объект помечается как
noscanдля сборщика мусора).
Сборка мусора (GC) и аллокатор
Аллокатор тесно интегрирован со сборщиком мусора (Garbage Collector):
- Цветность памяти: Аллокатор использует три-color маркировку (чёрный, серый, белый) для отслеживания живых объектов.
- Write Barrier: При обновлении указателей во время работы программы срабатывает барьер записи, который информирует GC об изменениях в графе объектов.
- Освобождение памяти: Когда GC определяет объект как мёртвый, его память не возвращается ОС немедленно, а помечается как свободная в соответствующем спане. Этот спан затем может быть повторно использован mcache или mcentral. Возврат памяти ОС происходит редко и большими регионами, только при значительном и длительном простаивании.
Ключевые оптимизации
- Отсутствие блокировок на fast-path: Большинство аллокаций (малые объекты) выполняются из локального mcache без синхронизации.
- Предвыделение (Cacheing): mcache заранее получает несколько спанов от mcentral, чтобы уменьшить частоту дорогих обращений к mheap.
- Tiny allocator: Особый путь для объектов размером <16 байт, которые могут размещаться в одном блоке памяти для уменьшения внешней фрагментации.
- Локализация данных: Поскольку каждая горутина работает со своим mcache, объекты, выделенные вместе, часто оказываются в одной области памяти, что улучшает кэш-локалимость CPU.
Пример в профилировщике
Используя pprof, можно увидеть работу аллокатора:
go tool pprof -alloc_objects http://localhost:6060/debug/pprof/heap
Это покажет, объекты каких размеров и где в программе создаются чаще всего, что помогает оптимизировать использование памяти.
Таким образом, Memory Allocator в Go — это высокооптимизированная система, которая балансирует между скоростью выделения (минимизируя блокировки), эффективным использованием памяти (через классы размеров) и тесной интеграцией со сборщиком мусора для автоматического управления жизненным циклом объектов.