Открыть сервис

Кэш потоков

Кэш потоков (англ. thread cache, thread-local cache) — это механизм управления памятью, при котором каждый поток выполнения (thread) операционной системы или пользовательского приложения получает собственный выделенный пул предварительно выделенных блоков памяти (кэш) для быстрого выделения и освобождения небольших объёмов данных без обращения к глобальному распределителю памяти (аллокатору). Основная цель кэша потоков — снижение накладных расходов на синхронизацию (блокировки, мьютексы, атомарные операции) и уменьшение фрагментации памяти, характерных для многопоточных приложений.

Принцип работы

В многопоточных средах, где множество потоков одновременно запрашивают и освобождают память, глобальный аллокатор (например, malloc в libc или new в C++) становится узким местом: каждый вызов требует синхронизации доступа к общим структурам данных (свободным спискам, кучам). Кэш потоков решает эту проблему следующим образом:

  1. Локальное выделение: Каждый поток при старте или при первом запросе памяти получает из глобального пула некоторый объём памяти (например, несколько страниц или блоков фиксированного размера). Этот объём становится его локальным кэшем.
  2. Быстрые операции: Выделение и освобождение небольших объектов (обычно до 64–256 КБ) выполняются исключительно в пределах локального кэша потока без блокировок. Для этого используются простые структуры, такие как свободные списки (free lists) или битовые карты (bitmaps).
  3. Пополнение и возврат: Когда локальный кэш исчерпан (не хватает свободных блоков), поток запрашивает дополнительную память у глобального аллокатора. Если кэш переполнен (например, после освобождения многих объектов), поток может вернуть часть памяти обратно глобальному пулу, чтобы избежать нерационального расхода ресурсов.
  4. Балансировка: Для предотвращения ситуации, когда один поток накапливает избыток памяти, а другим её не хватает, современные реализации (например, в 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 →