Кэш потоков
Кэш потоков (англ. thread cache, thread-local cache) — это механизм управления памятью, при котором каждый поток выполнения (thread) операционной системы или пользовательского приложения получает собственный выделенный пул предварительно выделенных блоков памяти (кэш) для быстрого выделения и освобождения небольших объёмов данных без обращения к глобальному распределителю памяти (аллокатору). Основная цель кэша потоков — снижение накладных расходов на синхронизацию (блокировки, мьютексы, атомарные операции) и уменьшение фрагментации памяти, характерных для многопоточных приложений.
Принцип работы
В многопоточных средах, где множество потоков одновременно запрашивают и освобождают память, глобальный аллокатор (например, malloc в libc или new в C++) становится узким местом: каждый вызов требует синхронизации доступа к общим структурам данных (свободным спискам, кучам). Кэш потоков решает эту проблему следующим образом:
- Локальное выделение: Каждый поток при старте или при первом запросе памяти получает из глобального пула некоторый объём памяти (например, несколько страниц или блоков фиксированного размера). Этот объём становится его локальным кэшем.
- Быстрые операции: Выделение и освобождение небольших объектов (обычно до 64–256 КБ) выполняются исключительно в пределах локального кэша потока без блокировок. Для этого используются простые структуры, такие как свободные списки (free lists) или битовые карты (bitmaps).
- Пополнение и возврат: Когда локальный кэш исчерпан (не хватает свободных блоков), поток запрашивает дополнительную память у глобального аллокатора. Если кэш переполнен (например, после освобождения многих объектов), поток может вернуть часть памяти обратно глобальному пулу, чтобы избежать нерационального расхода ресурсов.
- Балансировка: Для предотвращения ситуации, когда один поток накапливает избыток памяти, а другим её не хватает, современные реализации (например, в tcmalloc или jemalloc) используют механизмы перераспределения — «ленивую» передачу кэша между потоками или периодическую сборку.
Виды и реализации
Кэш потоков является ключевым компонентом многих современных аллокаторов памяти. Основные реализации различаются по стратегии управления локальными кэшами и размеру блоков.
tcmalloc (Thread-Caching Malloc)
Разработан компанией Google (входит в библиотеку gperftools). Использует иерархическую структуру: глобальный центральный кэш (central cache) и локальные кэши потоков (thread caches). Поток выделяет память из своего кэша, а при его исчерпании обращается к центральному кэшу, который, в свою очередь, может запрашивать страницы у операционной системы. tcmalloc оптимизирован для многопоточных приложений на C/C++ и широко применяется в серверных нагрузках (например, в веб-серверах).
jemalloc
Разработан Джейсоном Эвансом для FreeBSD, затем портирован на Linux и другие платформы. Использует концепцию «арен» (arenas) — независимых областей памяти, каждая из которых может обслуживать несколько потоков. Кэш потоков в jemalloc реализован через «tcache» (thread cache) — локальный пул блоков фиксированных размеров, поддерживаемый для каждого потока. jemalloc известен высокой производительностью при интенсивной многопоточности и низкой фрагментацией.
mimalloc
Разработан Microsoft Research. Отличается компактной реализацией и использованием «локальных кэшей» (local caches) на основе страниц. mimalloc минимизирует накладные расходы на синхронизацию, применяя только операции с атомарными счётчиками, и подходит для приложений с большим числом потоков (например, в игровых движках).
Стандартные аллокаторы
В стандартной библиотеке C (glibc) функция malloc использует глобальный аллокатор с блокировками, что может приводить к снижению производительности в многопоточных сценариях. Однако в glibc 2.26+ (2017 год) был добавлен механизм «per-thread cache» (ptcache), который частично решает проблему, но менее эффективен, чем специализированные решения.
Преимущества и недостатки
Преимущества
- Высокая производительность: Снижение накладных расходов на синхронизацию в многопоточных приложениях. В тестах (например, в бенчмарках tcmalloc) ускорение выделения/освобождения памяти может достигать 5–10 раз по сравнению с глобальным аллокатором.
- Уменьшение фрагментации: Локальные кэши позволяют избежать смешивания объектов разных размеров в одном пуле, что снижает внешнюю фрагментацию.
- Масштабируемость: При увеличении числа потоков производительность не падает катастрофически, поскольку каждый поток работает с собственным кэшем.
Недостатки
- Увеличение потребления памяти: Каждый поток резервирует некоторый объём памяти для своего кэша, даже если не использует его активно. В приложениях с сотнями или тысячами потоков это может привести к значительному перерасходу оперативной памяти.
- Сложность балансировки: При неравномерной нагрузке (один поток интенсивно выделяет память, а другие — нет) может возникнуть дисбаланс: один поток накапливает избыток кэша, а глобальный пул истощается.
- Проблемы с возвратом памяти: Локальные кэши могут долго удерживать освобождённую память, не возвращая её операционной системе, что увеличивает «виртуальный» размер процесса (RSS).
Применение
Кэш потоков широко используется в программном обеспечении, где критична производительность многопоточных операций с памятью:
- Веб-серверы и базы данных: Nginx, Apache, MySQL, PostgreSQL (в некоторых конфигурациях) используют tcmalloc или jemalloc для ускорения обработки запросов.
- Игровые движки: Unreal Engine, Unity — применяют mimalloc или собственные реализации кэша потоков для минимизации задержек при рендеринге и физике.
- Научные вычисления и машинное обучение: TensorFlow, PyTorch, OpenCV — используют аллокаторы с кэшем потоков для работы с многопоточными вычислениями на GPU и CPU.
- Операционные системы: Ядро Linux (начиная с версии 2.6) использует механизм slab allocator, который, по сути, является формой кэша потоков для ядерных структур данных.
Интересные факты
- В 2000-х годах, до широкого внедрения кэша потоков, многопоточные приложения на C++ часто страдали от «конкуренции за память» (memory contention), что приводило к падению производительности на многоядерных процессорах.
- В языке Rust стандартный аллокатор (std::alloc) по умолчанию использует jemalloc (на платформах, где это возможно), что обеспечивает высокую производительность в многопоточных программах.
- В 2023 году в проекте mimalloc была добавлена поддержка «ленивого» возврата памяти (lazy decommit), что позволило снизить пиковое потребление памяти в приложениях с большим числом потоков.
Источники
- «Thread-Caching Malloc» — документация gperftools (Google).
- «jemalloc: A Scalable Concurrent Malloc» — Jason Evans, 2006.
- «mimalloc: Free List Sharding in Action» — Microsoft Research, 2019.
- «The Linux Kernel: Slab Allocator» — документация ядра Linux.
- «Performance of Memory Allocators in Multithreaded Applications» — статьи на ресурсах IBM Developer и LWN.net.
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →