Malloc_trim (0) Освобождает Fastbins из Thread Arenas?

В течение прошлой недели или около того я изучал проблему в приложении, где память использует накопленные данные со временем. Я сузил его до строки, которая копирует

std::vector< std::vector< std::vector< std::map< uint, map< uint, std::bitset< N> > > > > >

в рабочем потоке (я понимаю, что это смехотворный способ организовать память). На регулярной основе рабочий поток уничтожается, воссоздается и эта структура памяти копируется потоком при его запуске. Исходные данные, которые копируются, передаются в рабочий поток по ссылке из основного потока.

Используя malloc_stat и malloc_info, я вижу, что когда рабочий поток уничтожается, используемая арена/куча сохраняет память, используемую для этой структуры, в ее свободном списке fastbins. Это имеет смысл, так как существует много индивидуальных распределений менее 64 байт.

Проблема заключается в том, что когда рабочий поток воссоздается, он создает новую арену/кучу вместо повторного использования предыдущего, так что fastbins из предыдущих аренов/кучи никогда не используются повторно. В конце концов система исчерпала память, прежде чем повторно использовать предыдущую кучу/арену, чтобы повторно использовать fastbins, на которые они держатся.

Несколько случайно я обнаружил, что вызов malloc_trim (0) в моем основном потоке после присоединения к рабочему потоку вызывает освобождение fastbins в потоках arenas/heaps. Насколько мне известно, это поведение недокументировано. У кого-нибудь есть объяснение?

Вот некоторый тестовый код, который я использую, чтобы увидеть это поведение:

// includes
#include <stdio.h>
#include <algorithm>
#include <vector>
#include <iostream>
#include <stdexcept>
#include <stdio.h>
#include <string>
#include <mcheck.h>
#include <malloc.h>
#include <map>
#include <bitset>
#include <boost/thread.hpp>
#include <boost/shared_ptr.hpp>

// Number of bits per bitset.
const int sizeOfBitsets = 40;

// Executes a system command. Used to get output of "free -m".
std::string ExecuteSystemCommand(const char* cmd) {
    char buffer[128];
    std::string result = "";
    FILE* pipe = popen(cmd, "r");
    if (!pipe) throw std::runtime_error("popen() failed!");
    try {
        while (!feof(pipe)) {
            if (fgets(buffer, 128, pipe) != NULL)
                result += buffer;
        }
    } catch (...) {
        pclose(pipe);
        throw;
    }
    pclose(pipe);
    return result;
}

// Prints output of "free -m" and output of malloc_stat().
void PrintMemoryStats()
{
    try
    {
        char *buf;
        size_t size;
        FILE *fp;

        std::string myCommand("free -m");
        std::string result = ExecuteSystemCommand(myCommand.c_str());
        printf("Free memory is \n%s\n", result.c_str());

        malloc_stats();

        fp = open_memstream(&buf, &size);
        malloc_info(0, fp);
        fclose(fp);
        printf("# Memory Allocation Stats\n%s\n#> ", buf);
        free(buf);

    }
    catch(...)
    {
        printf("Unable to print memory stats.\n");
        throw;
    }
}

void MakeCopies(std::vector<std::vector<std::map<uint, std::map<uint, std::bitset<sizeOfBitsets> > > > >& data)
{
    try
    {
        // Create copies.
        std::vector<std::vector<std::map<uint, std::map<uint, std::bitset<sizeOfBitsets> > > > > dataCopyA(data);
        std::vector<std::vector<std::map<uint, std::map<uint, std::bitset<sizeOfBitsets> > > > > dataCopyB(data);
        std::vector<std::vector<std::map<uint, std::map<uint, std::bitset<sizeOfBitsets> > > > > dataCopyC(data);

        // Print memory info.
        printf("Memory after creating data copies:\n");
        PrintMemoryStats();
    }
    catch(...)
    {
        printf("Unable to make copies.");
        throw;
    }
}

int main(int argc, char** argv)
{
    try
    {
          // When uncommented, disables the use of fastbins.
//        mallopt(M_MXFAST, 0);

        // Print memory info.
        printf("Memory to start is:\n");
        PrintMemoryStats();

        // Sizes of original data.
        int sizeOfDataA = 2048;
        int sizeOfDataB = 4;
        int sizeOfDataC = 128;
        int sizeOfDataD = 20;
        std::vector<std::vector<std::map<uint, std::map<uint, std::bitset<sizeOfBitsets> > > > > testData;

        // Populate data.
        testData.resize(sizeOfDataA);
        for(int a = 0; a < sizeOfDataA; ++a)
        {
            testData.at(a).resize(sizeOfDataB);
            for(int b = 0; b < sizeOfDataB; ++b)
            {
                for(int c = 0; c < sizeOfDataC; ++c)
                {
                    std::map<uint, std::bitset<sizeOfBitsets> > dataMap;
                    testData.at(a).at(b).insert(std::pair<uint, std::map<uint, std::bitset<sizeOfBitsets> > >(c, dataMap));
                    for(int d = 0; d < sizeOfDataD; ++d)
                    {
                        std::bitset<sizeOfBitsets> testBitset;
                        testData.at(a).at(b).at(c).insert(std::pair<uint, std::bitset<sizeOfBitsets> >(d, testBitset));
                    }
                }
            }
        }

        // Print memory info.
        printf("Memory to after creating original data is:\n");
        PrintMemoryStats();

        // Start thread to make copies and wait to join.
        {
            boost::shared_ptr<boost::thread> makeCopiesThread = boost::shared_ptr<boost::thread>(new boost::thread(&MakeCopies, boost::ref(testData)));
            makeCopiesThread->join();
        }

        // Print memory info.
        printf("Memory to after joining thread is:\n");
        PrintMemoryStats();

        malloc_trim(0);

        // Print memory info.
        printf("Memory to after malloc_trim(0) is:\n");
        PrintMemoryStats();

        return 0;

    }
    catch(...)
    {
        // Log warning.
        printf("Unable to run application.");

        // Return failure.
        return 1;
    }

    // Return success.
    return 0;
}

Интересный результат до и после вызова обрезки malloc (смотрите "СМОТРИТЕ ЗДЕСЬ!" ):

#> Memory to after joining thread is:
Free memory is
              total        used        free      shared  buff/cache   available
Mem:         257676        7361      246396          25        3918      249757
Swap:          1023           0        1023

Arena 0:
system bytes     = 1443450880
in use bytes     = 1443316976
Arena 1:
system bytes     =   35000320
in use bytes     =       6608
Total (incl. mmap):
system bytes     = 1478451200
in use bytes     = 1443323584
max mmap regions =          0
max mmap bytes   =          0
# Memory Allocation Stats
<malloc version="1">
<heap nr="0">
<sizes>
<size from="241" to="241" total="241" count="1"/>
<size from="529" to="529" total="529" count="1"/>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="2" size="770"/>
<system type="current" size="1443450880"/>
<system type="max" size="1443459072"/>
<aspace type="total" size="1443450880"/>
<aspace type="mprotect" size="1443450880"/>
</heap>
<heap nr="1">
<sizes>
<size from="33" to="48" total="48" count="1"/>
<size from="49" to="64" total="4026531712" count="62914558"/> <-- LOOK HERE!
<size from="65" to="80" total="160" count="2"/>
<size from="81" to="96" total="301989888" count="3145728"/> <-- LOOK HERE!
<size from="33" to="33" total="231" count="7"/>
<size from="49" to="49" total="1274" count="26"/>
<unsorted from="0" to="49377" total="1431600" count="6144"/>
</sizes>
<total type="fast" count="66060289" size="4328521808"/>
<total type="rest" count="6177" size="1433105"/>
<system type="current" size="4329967616"/>
<system type="max" size="4329967616"/>
<aspace type="total" size="35000320"/>
<aspace type="mprotect" size="35000320"/>
</heap>
<total type="fast" count="66060289" size="4328521808"/>
<total type="rest" count="6179" size="1433875"/>
<total type="mmap" count="0" size="0"/>
<system type="current" size="5773418496"/>
<system type="max" size="5773426688"/>
<aspace type="total" size="1478451200"/>
<aspace type="mprotect" size="1478451200"/>
</malloc>

#> Memory to after malloc_trim(0) is:
Free memory is
              total        used        free      shared  buff/cache   available
Mem:         257676        3269      250488          25        3918      253850
Swap:          1023           0        1023

Arena 0:
system bytes     = 1443319808
in use bytes     = 1443316976
Arena 1:
system bytes     =   35000320
in use bytes     =       6608
Total (incl. mmap):
system bytes     = 1478320128
in use bytes     = 1443323584
max mmap regions =          0
max mmap bytes   =          0
# Memory Allocation Stats
<malloc version="1">
<heap nr="0">
<sizes>
<size from="209" to="209" total="209" count="1"/>
<size from="529" to="529" total="529" count="1"/>
<unsorted from="0" to="49377" total="1431600" count="6144"/>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="6146" size="1432338"/>
<system type="current" size="1443459072"/>
<system type="max" size="1443459072"/>
<aspace type="total" size="1443459072"/>
<aspace type="mprotect" size="1443459072"/>
</heap>
<heap nr="1"> <---------------------------------------- LOOK HERE!
<sizes> <-- HERE!
<unsorted from="0" to="67108801" total="4296392384" count="6208"/>
</sizes>
<total type="fast" count="0" size="0"/>
<total type="rest" count="6208" size="4296392384"/>
<system type="current" size="4329967616"/>
<system type="max" size="4329967616"/>
<aspace type="total" size="35000320"/>
<aspace type="mprotect" size="35000320"/>
</heap>
<total type="fast" count="0" size="0"/>
<total type="rest" count="12354" size="4297824722"/>
<total type="mmap" count="0" size="0"/>
<system type="current" size="5773426688"/>
<system type="max" size="5773426688"/>
<aspace type="total" size="1478459392"/>
<aspace type="mprotect" size="1478459392"/>
</malloc>

#>

Документация на выходе malloc_info практически отсутствует, поэтому я не был уверен, что те выходы, которые я указал, были действительно быстрыми бункерами. Чтобы убедиться, что они действительно являются fastbins, я раскомментирую строку кода

mallopt(M_MXFAST, 0);

чтобы отключить использование fastbins и использование памяти для кучи 1 после присоединения к потоку, перед вызовом malloc_trim (0) выглядит так, как будто он работает с включенными fastbins после вызова malloc_trim (0). Самое главное, отключение использования fastbins возвращает память в систему сразу же после присоединения потока. Вызов malloc_trim (0) после присоединения потока с включенными fastbins также возвращает память в систему.

В документации для malloc_trim (0) указано, что она может освобождать память только из верхней части основной кучи арены, так что здесь происходит? Я работаю на CentOS 7 с glibc версии 2.17.

Ответ 1

malloc_trim (0) утверждает, что он может освобождать память только от вершины главной кучи арены, так что происходит здесь?

Его можно назвать "устаревшей" или "неправильной" документацией. Glibc не имеет документация функции malloc_trim; и Linux использует man-страницы из проекта man-pages. Страница руководства malloc_trim http://man7.org/linux/man-pages/man3/malloc_trim.3.html была написана в 2012 году соавтором man-страниц как новый. Вероятно, он использовал некоторые комментарии из исходного кода glibc malloc/malloc.c http://code.metager.de/source/xref/gnu/glibc/malloc/malloc.c#675

676  malloc_trim(size_t pad);
677
678  If possible, gives memory back to the system (via negative
679  arguments to sbrk) if there is unused memory at the `high' end of
680  the malloc pool. You can call this after freeing large blocks of
681  memory to potentially reduce the system-level memory requirements
682  of a program. However, it cannot guarantee to reduce memory. Under
683  some allocation patterns, some large free blocks of memory will be
684  locked between two used chunks, so they cannot be given back to
685  the system.
686
687  The `pad' argument to malloc_trim represents the amount of free
688  trailing space to leave untrimmed. If this argument is zero,
689  only the minimum amount of memory to maintain internal data
690  structures will be left (one page or less). Non-zero arguments
691  can be supplied to maintain enough trailing space to service
692  future expected allocations without having to re-obtain memory
693  from the system.
694
695  Malloc_trim returns 1 if it actually released any memory, else 0.
696  On systems that do not support "negative sbrks", it will always
697  return 0.

Фактическая реализация в glibc __malloc_trim и имеет код для итерации по аренам:

http://code.metager.de/source/xref/gnu/glibc/malloc/malloc.c#4552

4552 int
4553 __malloc_trim (size_t s)

4560  mstate ar_ptr = &main_arena;
4561  do
4562    {
4563      (void) mutex_lock (&ar_ptr->mutex);
4564      result |= mtrim (ar_ptr, s);
4565      (void) mutex_unlock (&ar_ptr->mutex);
4566
4567      ar_ptr = ar_ptr->next;
4568    }
4569  while (ar_ptr != &main_arena);

Каждая арена обрезается с помощью функции mtrim() (mtrim()), которая вызывает malloc_consolidate(), чтобы преобразовать все свободные сегменты из fastbins (они не объединены свободными, поскольку они бывают быстрыми) к нормальным свободным кускам (которые объединены с соседними кусками)

4498  /* Ensure initialization/consolidation */
4499  malloc_consolidate (av);

4111  malloc_consolidate is a specialized version of free() that tears
4112  down chunks held in fastbins. 

1581   Fastbins
1591    Chunks in fastbins keep their inuse bit set, so they cannot
1592    be consolidated with other free chunks. malloc_consolidate
1593    releases all chunks in fastbins and consolidates them with
1594    other free chunks.

Проблема в том, что когда рабочий поток воссоздан, он создает новую арену/кучу, а не повторно использует предыдущую, так что fastbins из предыдущих аренов/кучек никогда не используются повторно.

Это странно. По дизайну максимальное количество аренов ограничено в glibc malloc cpu_core_count * 8 (для 64-битной платформы); cpu_core_count * 2 (для 32-разрядной платформы) или переменная среды MALLOC_ARENA_MAX/mallopt параметр M_ARENA_MAX.

Вы можете ограничить количество арена для своей заявки; вызовите malloc_trim() периодически или вызовите malloc() с "большим" размером (он вызовет malloc_consolidate), а затем free() для него из ваших потоков непосредственно перед уничтожением:

3319 _int_malloc (mstate av, size_t bytes)
3368  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
 // fastbin allocation path
3405  if (in_smallbin_range (nb))
 // smallbin path; malloc_consolidate may be called
3437     If this is a large request, consolidate fastbins before continuing.
3438     While it might look excessive to kill all fastbins before
3439     even seeing if there is space available, this avoids
3440     fragmentation problems normally associated with fastbins.
3441     Also, in practice, programs tend to have runs of either small or
3442     large requests, but less often mixtures, so consolidation is not
3443     invoked all that often in most programs. And the programs that
3444     it is called frequently in otherwise tend to fragment.
3445   */
3446
3447  else
3448    {
3449      idx = largebin_index (nb);
3450      if (have_fastchunks (av))
3451        malloc_consolidate (av);
3452    }

PS: есть комментарий на странице руководства malloc_trim https://github.com/mkerrisk/man-pages/commit/a15b0e60b297e29c825b7417582a33e6ca26bf65:

+.SH NOTES
+This function only releases memory in the main arena.
+.\" malloc/malloc.c::mTRIm():
+.\" return result | (av == &main_arena ? sYSTRIm (pad, av) : 0);

Да, есть проверка на main_arena, но она находится на самом конце реализации malloc_trim mtrim(), и это просто для вызова sbrk() с отрицательным смещением. С 2007 года (glibc 2.9 и новее) существует другой метод для возврата обратно в ОС: madvise(MADV_DONTNEED), который используется во всех аренах ( и не документируется автором glibc-патча или автором man-страницы). Консолидация называется для каждой арены. Существует также код для обрезки (munmapping) верхнего куска mmap-ed heaps (heap_trim/shrink_heap, вызванного из медленного пути free()), но он не вызывается из malloc_trim.