mimalloc для C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

mimalloc для C++: высокопроизводительный аллокатор памяти

В современном C++-разработке управление памятью остаётся одной из ключевых задач, влияющих на производительность приложений. Стандартный аллокатор (malloc из glibc или jemalloc) часто не справляется с нагрузками в многопоточных средах или при интенсивном выделении/освобождении мелких блоков. mimalloc (сокращение от "microsoft mimalloc") — это компактный, но чрезвычайно быстрый аллокатор общего назначения, разработанный Microsoft Research. Он демонстрирует впечатляющие результаты: до 2-3 раз быстрее malloc в многопоточных сценариях и значительно меньшее потребление памяти.

Зачем использовать mimalloc? Во-первых, он оптимизирован для современных многоядерных процессоров: использует локальные кэши потоков (thread-local caches) и минимизирует конкуренцию за глобальные блокировки. Во-вторых, он эффективно борется с фрагментацией памяти благодаря алгоритму "free list sharding" — разбиению списков свободных блоков по размеру и потокам. В-третьих, mimalloc имеет встроенную защиту от некоторых типов уязвимостей (double-free, use-after-free) и поддерживает "guarded allocations" для отладки. Это делает его идеальным выбором для высоконагруженных серверов, игровых движков, СУБД и любого C++-кода, где важна скорость работы с памятью.

В этой статье мы подробно разберём, как установить mimalloc, как использовать его основные функции, и приведём реальные примеры интеграции в C++ проекты. Вы узнаете, как заменить стандартный malloc всего одной строкой кода и получить прирост производительности до 30-50% без изменения бизнес-логики.

Установка

mimalloc можно установить несколькими способами. Рекомендуемый способ — через менеджер пакетов vcpkg или Conan, либо собрать из исходников.

Через vcpkg (рекомендуется для Windows/Linux/macOS)

# Установите vcpkg, если ещё не установлен
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh  # или .\bootstrap-vcpkg.bat на Windows
./vcpkg integrate install

# Установка mimalloc
./vcpkg install mimalloc

Через Conan

conan install mimalloc/2.1.6@

Сборка из исходников (Linux/macOS)

git clone https://github.com/microsoft/mimalloc
cd mimalloc
mkdir -p build && cd build
cmake ..
make -j$(nproc)
sudo make install

Подключение к проекту CMake

find_package(mimalloc REQUIRED)
target_link_libraries(my_target PRIVATE mimalloc::mimalloc)

Быстрый старт: минимальный рабочий пример

Самый простой способ начать использовать mimalloc — заменить стандартный malloc/new на его версию. Для этого достаточно переопределить глобальные операторы new/delete.

#include <iostream>
#include <mimalloc.h>
#include <mimalloc-new-delete.h>  // переопределяет глобальные new/delete

int main() {
    // Теперь все new/delete используют mimalloc
    int* arr = new int[1000];
    for (int i = 0; i < 1000; ++i) arr[i] = i;
    std::cout << "Sum: " << arr[500] << std::endl;
    delete[] arr;

    // Можно также использовать C-функции напрямую
    void* p = mi_malloc(64);
    mi_free(p);

    return 0;
}

Скомпилируйте с линковкой к mimalloc:

g++ -std=c++17 main.cpp -lmimalloc -o test

Основные методы и функции

mimalloc предоставляет как C-совместимый API (функции с префиксом mi_), так и C++-обёртки. Рассмотрим 8 ключевых функций.

1. mi_malloc — выделение памяти

Сигнатура: void* mi_malloc(size_t size)

Что делает: Выделяет блок памяти размером size байт. Аналог стандартного malloc, но оптимизирован для многопоточности.

#include <mimalloc.h>
#include <cstring>

int main() {
    void* p = mi_malloc(256);
    if (p) {
        std::memset(p, 0, 256);
        // работаем с памятью
        mi_free(p);
    }
    return 0;
}

2. mi_calloc — выделение с обнулением

Сигнатура: void* mi_calloc(size_t count, size_t size)

Что делает: Выделяет память для массива из count элементов по size байт каждый и обнуляет всю выделенную область.

#include <mimalloc.h>
#include <iostream>

int main() {
    int* arr = (int*)mi_calloc(10, sizeof(int));
    for (int i = 0; i < 10; ++i) {
        std::cout << arr[i] << " "; // все нули
    }
    mi_free(arr);
    return 0;
}

3. mi_realloc — перераспределение памяти

Сигнатура: void* mi_realloc(void* p, size_t newsize)

Что делает: Изменяет размер ранее выделенного блока до newsize. Содержимое сохраняется до минимального из старого и нового размера.

#include <mimalloc.h>
#include <cstring>

int main() {
    char* buf = (char*)mi_malloc(10);
    std::strcpy(buf, "Hello");
    buf = (char*)mi_realloc(buf, 20);
    std::strcat(buf, " World!");
    mi_free(buf);
    return 0;
}

4. mi_free — освобождение памяти

Сигнатура: void mi_free(void* p)

Что делает: Освобождает блок памяти, ранее выделенный через mi_malloc/mi_calloc/mi_realloc. Безопасен для вызова с nullptr.

#include <mimalloc.h>

int main() {
    void* p = mi_malloc(100);
    mi_free(p);
    mi_free(nullptr); // безопасно, ничего не делает
    return 0;
}

5. mi_malloc_aligned — выравненное выделение

Сигнатура: void* mi_malloc_aligned(size_t size, size_t alignment)

Что делает: Выделяет память с указанным выравниванием (alignment должен быть степенью двойки). Полезно для SIMD-инструкций (SSE, AVX).

#include <mimalloc.h>
#include <immintrin.h> // для AVX

int main() {
    // выделяем 32-байтовое выравненный массив для AVX
    float* data = (float*)mi_malloc_aligned(1024 * sizeof(float), 32);
    __m256 vec = _mm256_load_ps(data); // безопасная загрузка
    mi_free_aligned(data);
    return 0;
}

6. mi_stats_print — вывод статистики

Сигнатура: void mi_stats_print(mi_output_fun* out, void* arg) или void mi_stats_print_out(mi_output_fun* out, void* arg)

Что делает: Выводит подробную статистику использования памяти: выделено, освобождено, текущий размер кучи, количество вызовов и т.д. Полезно для профилирования.

#include <mimalloc.h>
#include <cstdio>

void my_output(const char* msg, void* arg) {
    fprintf((FILE*)arg, "%s", msg);
}

int main() {
    for (int i = 0; i < 1000; ++i) {
        void* p = mi_malloc(rand() % 1024);
        mi_free(p);
    }
    mi_stats_print(my_output, stderr);
    return 0;
}

7. mi_heap_new / mi_heap_delete — изолированные кучи

Сигнатура: mi_heap_t* mi_heap_new() и void mi_heap_delete(mi_heap_t* heap)

Что делает: Создаёт/удаляет изолированную кучу. Все выделения в этой куче не пересекаются с глобальной. Полезно для плагинов или для освобождения всей памяти разом.

#include <mimalloc.h>

int main() {
    mi_heap_t* my_heap = mi_heap_new();
    void* p1 = mi_heap_malloc(my_heap, 100);
    void* p2 = mi_heap_malloc(my_heap, 200);
    // ... работаем
    mi_heap_delete(my_heap); // освобождает ВСЮ память в куче
    return 0;
}

8. mi_heap_malloc — выделение в конкретной куче

Сигнатура: void* mi_heap_malloc(mi_heap_t* heap, size_t size)

Что делает: Выделяет память в указанной куче, а не в глобальной. Аналогично mi_malloc, но с явным указанием кучи.

#include <mimalloc.h>
#include <vector>

int main() {
    mi_heap_t* heap = mi_heap_new();
    std::vector<int, mi_heap_stl_allocator<int>> vec(heap);
    vec.push_back(42);
    mi_heap_delete(heap);
    return 0;
}

Реальный практический пример: многопоточный сервер

Рассмотрим пример многопоточного HTTP-сервера (упрощённого), который обрабатывает запросы и активно выделяет/освобождает память. Сравним производительность с стандартным malloc.

#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <chrono>
#include <mimalloc.h>
#include <mimalloc-new-delete.h>

// Имитация обработки HTTP-запроса
std::string process_request(int id) {
    // Выделяем много мелких блоков — типично для парсинга
    std::string header = "HTTP/1.1 200 OK\r\n";
    std::string body = "<html><body><h1>Hello from thread " 
                       + std::to_string(id) + "</h1></body></html>";
    std::string response = header + "Content-Length: " 
                          + std::to_string(body.size()) + "\r\n\r\n" + body;
    
    // Имитация работы с кучей мелких объектов
    std::vector<int> temp_buffer;
    for (int i = 0; i < 100; ++i) {
        temp_buffer.push_back(i * id);
    }
    
    return response;
}

void worker_thread(int id, int iterations) {
    for (int i = 0; i < iterations; ++i) {
        std::string resp = process_request(id);
        // Имитация отправки ответа
        volatile size_t len = resp.size(); // предотвращаем оптимизацию
        (void)len;
    }
}

int main() {
    const int NUM_THREADS = 8;
    const int ITERATIONS = 10000;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    std::vector<std::thread> threads;
    for (int i = 0; i < NUM_THREADS; ++i) {
        threads.emplace_back(worker_thread, i, ITERATIONS);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "Total time: " << duration.count() << " ms" << std::endl;
    
    // Вывод статистики mimalloc
    mi_stats_print(nullptr, nullptr);
    
    return 0;
}

Этот пример можно скомпилировать с mimalloc и без него (закомментировав #include <mimalloc-new-delete.h>). На многоядерных системах вы увидите прирост производительности в 1.5-3 раза.

Советы по производительности и типичные ошибки

Советы по производительности:

  1. Используйте thread-local кучи. Если у вас есть "горячий" поток, который активно выделяет/освобождает память, создайте для него отдельную кучу через mi_heap_new() и работайте с ней. Это снизит конкуренцию за глобальные структуры.
  2. Избегайте частых realloc. mimalloc оптимизирован для malloc/free, но realloc может быть менее эффективным. Если знаете точный размер заранее — выделяйте сразу.
  3. Используйте выравненные выделения для SIMD. Если ваш код использует SSE/AVX инструкции, выделяйте память с выравниванием 16/32 байта через mi_malloc_aligned.
  4. Освобождайте память в том же потоке, где выделяли. Хотя mimalloc поддерживает освобождение из другого потока, это может привести к дополнительным накладным расходам.
  5. Для отладки используйте guarded allocations. Включите опцию MI_GUARDED при сборке, чтобы ловить выход за границы буфера.

Типичные ошибки:

  1. Смешивание аллокаторов.

Рекомендаций