Linux Allocator не выпускает небольшие куски памяти

Распределитель Linux glibc, похоже, ведет себя странно. Надеюсь, кто-то может пролить свет на это. Вот исходный файл, который у меня есть:

first.cpp:

#include <unistd.h>
#include <stdlib.h>
#include <list>
#include <vector>

int main() {

  std::list<char*> ptrs;
  for(size_t i = 0; i < 50000; ++i) {
    ptrs.push_back( new char[1024] );
  }
  for(size_t i = 0; i < 50000; ++i) {
    delete[] ptrs.back();
    ptrs.pop_back();
  }

  ptrs.clear();

  sleep(100);

  return 0;
}

second.cpp:

#include <unistd.h>
#include <stdlib.h>
#include <list>

int main() {

  char** ptrs = new char*[50000];
  for(size_t i = 0; i < 50000; ++i) {
    ptrs[i] = new char[1024];
  }
  for(size_t i = 0; i < 50000; ++i) {
    delete[] ptrs[i];
  }
  delete[] ptrs;

  sleep(100);

  return 0;
}

Я компилирую оба:

$ g++ -o first first.cpp
$ g++ -o second second.cpp

Я запускаю сначала, и после его сна я вижу размер резидентной памяти:

Когда я компилирую first.cpp и запускаю его, я смотрю на память с помощью ps:

$ ./first&
$ ps aux | grep first
davidw    9393  1.3  0.3  64344 53016 pts/4    S    23:37   0:00 ./first


$ ./second&
$ ps aux | grep second
davidw    9404  1.0  0.0  12068  1024 pts/4    S    23:38   0:00 ./second

Обратите внимание на размер резидентной памяти. Сначала размер резидентной памяти составляет 53016k. во втором - 1024k. Сначала никогда не отпускал выделение обратно в ядро ​​по той или иной причине.

Почему первая программа не освобождает память от ядра, а вторая программа? Я понимаю, что первая программа использует связанный список, и связанный список, вероятно, выделяет некоторые узлы на той же странице, что и данные, которые мы освобождаем. Однако эти узлы должны быть освобождены, поскольку мы выталкиваем эти узлы, а затем очищаем связанный список. Если вы запускаете любую из этих программ через valgrind, она возвращается без утечек памяти. Что, вероятно, происходит, так это то, что память фрагментируется в first.cpp, что не во втором. Cpp. Однако, если вся память на странице освобождена, как эта страница не будет возвращена обратно в ядро? Что нужно, чтобы память осталась обратно в ядро? Как я могу изменить first.cpp(продолжая помещать char * в список), чтобы память была оставлена ​​в ядре.

Ответы

Ответ 1

Такое поведение является преднамеренным, существует настраиваемый порог, который glibc использует для принятия решения о том, действительно ли вернуть память в систему или кэшировать ее для последующего повторного использования. В вашей первой программе вы делаете множество небольших распределений с каждым push_back, и эти небольшие выделения не являются смежным блоком и, предположительно, ниже порогового значения, поэтому не возвращаются в ОС.

Вызов malloc_trim(0) после очистки списка должен немедленно вызвать glibc верните самую верхнюю область свободной памяти в систему (требуется системный вызов sbrk в следующей памяти времени необходимо.)

Если вам действительно нужно переопределить поведение по умолчанию (которое я бы не рекомендовал, если профилирование не показывает, что это действительно помогает), то вам, вероятно, следует использовать strace и/или экспериментировать с mallinfo в посмотрите, что на самом деле происходит в вашей программе, и, возможно, используя mallopt, чтобы отрегулируйте порог для возврата памяти в систему.

Ответ 2

Он сохраняет меньшие куски в случае, если вы запросите их снова. Это простая оптимизация кэширования, а не поведение, о котором нужно беспокоиться.

Ответ 3

Как правило, память, выделенная new, возвращается только в систему, когда процесс завершается. Во втором случае я подозреваю, что libc использует специальный распределитель для очень больших непрерывных блоков, который возвращает его, но я был бы очень удивлен, если бы был возвращен любой из ваших new char[1024], а во многих Unices даже большой блок не будет возвращен.

Ответ 4

(Редактирование моего ответа, поскольку здесь действительно не проблема).

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

Когда использование памяти не является тем, что я ожидаю от нее в Linux, я обычно начинаю анализ с помощью инструмента mtrace и анализирует файл /proc/self/maps.

mtrace используется, скопировав свой код вокруг двух вызовов, один для запуска трассировки и один, который его завершает.

  mtrace();
  {
      // do stuff
  }
  muntrace();

Вызовы mtrace активны только в том случае, если установлена ​​переменная среды MALLOC_TRACE. Он определяет имя файла для вывода журнала mtrace. Затем этот вывод журнала может быть проанализирован на предмет утечки памяти. Для анализа вывода может использоваться программа командной строки под названием mtrace.

$ MALLOC_TRACE=mtrace.log ./a.out
$ mtrace ./a.out mtrace.log

В файле /proc/self/maps представлен список областей с отображением памяти, используемых текущей программой, включая анонимные регионы. Это может помочь выявить регионы, которые особенно велики, а затем необходимо дополнительное слежение, чтобы определить, с чем связана эта область. Ниже приведена простая программа для удаления файла /proc/self/maps в другой файл.

void dump_maps (const char *outfilename) {
  std::ifstream inmaps("/proc/self/maps");
  std::ofstream outf(outfilename, std::ios::out|std::ios::trunc);
  outf << inmaps.rdbuf();
}