В настоящее время я пытаюсь создать инструмент, который должен обрабатывать множество строк.
При тестировании инструмента с помощью top я заметил, что использование памяти моей программой со временем увеличивается. После длительного анализа и использования таких инструментов, как valgrind, я пришел к выводу, что это может быть не связано с моими собственными утечками памяти.
Сначала я подумал, что это может быть связано с фрагментацией памяти, но я исследовал дальше и заметил нечто, чему не могу найти объяснения.
#include <string>
#include <iostream>
#include <unistd.h>
#include <array>
define SIZE_LITTLE 100
define SIZE_BIG 500000
struct Test {
std::string data;
};
int main(){
{
{
std::string line = "very long string, i tested using a 15Mb string";
std::array<struct Test*, SIZE_LITTLE> arr;
for (int i = 0; i < SIZE_LITTLE; i++){
arr[i] = new Test{line};
}
std::cout << "Allocated" << std::endl;
sleep(5);
for (int i = 0; i < SIZE_LITTLE; i++){
delete arr[i];
}
std::cout << "Memory released" << std::endl;
sleep(5);
}
std::cout << "Memory released out of scope" << std::endl;
sleep(5);
}
{
{
std::string line = "smaller string";
std::array<struct Test*, SIZE_BIG> arr;
for (int i = 0; i < SIZE_BIG; i++){
arr[i] = new Test{line};
}
std::cout << "Allocated" << std::endl;
sleep(5);
for (int i = 0; i < SIZE_BIG; i++){
delete arr[i];
}
std::cout << "Memory released" << std::endl;
sleep(5);
}
std::cout << "Memory released out of scope" << std::endl;
sleep(5);
}
}
Когда я запускаю этот код и смотрю на потребление памяти, я не могу объяснить поведение.
Кажется, что первый блок кода ведет себя ожидаемо. Во время сна после распределения использование памяти является высоким. После выполнения удаления память возвращается к значению, близкому к 0, и массив выходит за пределы области видимости.
С другой стороны, второй блок имеет высокий уровень использования памяти после выделения, но он никогда не отключается ни после удаления всех указателей, ни после выхода за пределы области видимости.
Я также тестировал запуск второго блока дважды подряд и ожидал, что потребление памяти будет выше, но нет, оно остается неподвижным от начала до конца.
Что мне не хватает?
Почему каждая вторая строка пуста? Это делает видимым меньше кода и появляется полоса прокрутки.
@ 273K ISTR, что копирование из VS имеет тенденцию делать это.
@273K Да, извините за плохое отображение, не проверил копирование





Существует несколько уровней управления памятью. Предполагая, что вы используете Windows, позвольте мне объяснить для Windows.
Первый уровень представляет собой абстракцию аппаратного обеспечения. Таким образом, вы не сможете контролировать точный адрес физической памяти, к которой будет обращаться ваша программа. Вы можете себе представить хаос, если бы каждая программа конкурировала за одну и ту же физическую память. Вместо этого любой указатель, который у вас есть, будет ссылаться на виртуальную память. Таким образом, любой указатель предназначен только для вашей программы, а не для других программ. Это имеет и другие преимущества, например. что вы можете получить доступ к большему объему памяти, чем доступно в физической оперативной памяти.
Виртуальная память обычно имеет размер 64 КБ. Это означает, что когда вы запрашиваете 1 байт у диспетчера виртуальной памяти, он на самом деле выделяет 64 КБ, и это будет выглядеть как утечка памяти.
Для учета этих потерь в качестве второго уровня используется диспетчер кучи. Диспетчер кучи знает об этой трате виртуальной памяти и справляется с ней. Поэтому, когда вы запрашиваете 1 байт памяти, диспетчер кучи запросит у диспетчера виртуальной памяти 64 КБ и предоставит вам часть этой суммы. Когда вы позже запросите еще 1 байт, он предоставит вам еще одну часть того, что уже есть у диспетчера кучи. Таким образом, количество отходов сокращается. Только когда диспетчер кучи не может найти свободную память в уже имеющейся у него виртуальной памяти, он запросит новую виртуальную память.
Дело в том, что если вы вернете один из 2 байтов, диспетчер кучи пока не сможет вернуть 64 КБ диспетчеру виртуальной памяти, потому что вам все равно нужно иметь доступ к этому другому байту, поэтому он может все равно похоже на утечку.
Внутри вашего приложения может существовать третий уровень. Допустим, вы выделяете буфер размером 4096 байт и заполняете только 350 из них. Тогда оставшиеся 3746 байт «не используются». Однако ни диспетчер кучи, ни диспетчер виртуальной памяти не могут знать об этом, поэтому память будет отображаться как «используемая».
Почему же он ведет себя по-разному для строк разного размера?
Ну, небольшая строка, такая как «строка меньшего размера», требует всего 14 байт. Поэтому хорошей идеей будет хранить многие из них с помощью диспетчера кучи. С другой стороны, если в строке размером 15 МБ потеряно 65534 байта, это всего лишь 0,4% потерь, что может быть приемлемо.
Что делает диспетчер кучи: все, что превышает 512 КБ, напрямую пересылается в диспетчер виртуальной памяти. Материалы размером менее 512 КБ управляются диспетчером кучи.
Следовательно: когда вы освобождаете строку размером 15 МБ, диспетчер кучи может напрямую попросить диспетчер виртуальной памяти освободить все, поскольку память не используется совместно с каким-либо другим объектом. Когда вы освобождаете строку размером 14 байт, она не может этого сделать, поскольку память все еще может потребоваться для других строк.
Кроме того, диспетчер кучи может «кэшировать» эту виртуальную память для будущего использования, поэтому он вообще не будет ее возвращать, но будет готов к будущему выделению чего-либо.
В этом описании не учитываются многие детали, но их должно быть достаточно, чтобы понять эту проблему и возможные аналогичные проблемы. Будьте готовы это понять
Так что да, это немного сложно. Просто поверьте C++, диспетчеру кучи и диспетчеру виртуальной памяти, что они поступают правильно. Обычно они это делают.
Спасибо за объяснение. На самом деле я использую Linux, извините, что вам пришлось догадываться, но я прочитал больше о том, что вы объяснили, и концепции в основном те же. Теперь я понимаю, что менеджер кучи может удерживать фрагменты памяти, даже если каждая память внутри них освобождена. Но мне кажется странным, что он может удерживать столько уже освобожденной памяти.
Это не то, что контролируется C++ или вашей программой, это происходит на уровне ОС. Тот факт, что вы освобождаете память в своей программе, не означает, что ОС фактически отключит страницы виртуальной памяти от ваших процессов. Не удаляя сопоставление страниц, их можно использовать повторно. Но когда эти страницы потребуются другим процессам, они будут отключены от вашего процесса и сопоставлены с процессами, которым они нужны. Это не утечка, это особенность. Точный алгоритм работы зависит от ОС.