Почему массовое выделение строк приводит к увеличению использования памяти

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

При тестировании инструмента с помощью 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, и массив выходит за пределы области видимости.

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

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

Что мне не хватает?

Это не то, что контролируется C++ или вашей программой, это происходит на уровне ОС. Тот факт, что вы освобождаете память в своей программе, не означает, что ОС фактически отключит страницы виртуальной памяти от ваших процессов. Не удаляя сопоставление страниц, их можно использовать повторно. Но когда эти страницы потребуются другим процессам, они будут отключены от вашего процесса и сопоставлены с процессами, которым они нужны. Это не утечка, это особенность. Точный алгоритм работы зависит от ОС.

Some programmer dude 25.09.2023 09:57

Почему каждая вторая строка пуста? Это делает видимым меньше кода и появляется полоса прокрутки.

273K 25.09.2023 10:00

@ 273K ISTR, что копирование из VS имеет тенденцию делать это.

Botje 25.09.2023 10:27

@273K Да, извините за плохое отображение, не проверил копирование

Zoraphie 25.09.2023 12:47
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

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

Виртуальная память обычно имеет размер 64 КБ. Это означает, что когда вы запрашиваете 1 байт у диспетчера виртуальной памяти, он на самом деле выделяет 64 КБ, и это будет выглядеть как утечка памяти.

Для учета этих потерь в качестве второго уровня используется диспетчер кучи. Диспетчер кучи знает об этой трате виртуальной памяти и справляется с ней. Поэтому, когда вы запрашиваете 1 байт памяти, диспетчер кучи запросит у диспетчера виртуальной памяти 64 КБ и предоставит вам часть этой суммы. Когда вы позже запросите еще 1 байт, он предоставит вам еще одну часть того, что уже есть у диспетчера кучи. Таким образом, количество отходов сокращается. Только когда диспетчер кучи не может найти свободную память в уже имеющейся у него виртуальной памяти, он запросит новую виртуальную память.

Дело в том, что если вы вернете один из 2 байтов, диспетчер кучи пока не сможет вернуть 64 КБ диспетчеру виртуальной памяти, потому что вам все равно нужно иметь доступ к этому другому байту, поэтому он может все равно похоже на утечку.

Внутри вашего приложения может существовать третий уровень. Допустим, вы выделяете буфер размером 4096 байт и заполняете только 350 из них. Тогда оставшиеся 3746 байт «не используются». Однако ни диспетчер кучи, ни диспетчер виртуальной памяти не могут знать об этом, поэтому память будет отображаться как «используемая».

Почему же он ведет себя по-разному для строк разного размера?

Ну, небольшая строка, такая как «строка меньшего размера», требует всего 14 байт. Поэтому хорошей идеей будет хранить многие из них с помощью диспетчера кучи. С другой стороны, если в строке размером 15 МБ потеряно 65534 байта, это всего лишь 0,4% потерь, что может быть приемлемо.

Что делает диспетчер кучи: все, что превышает 512 КБ, напрямую пересылается в диспетчер виртуальной памяти. Материалы размером менее 512 КБ управляются диспетчером кучи.

Следовательно: когда вы освобождаете строку размером 15 МБ, диспетчер кучи может напрямую попросить диспетчер виртуальной памяти освободить все, поскольку память не используется совместно с каким-либо другим объектом. Когда вы освобождаете строку размером 14 байт, она не может этого сделать, поскольку память все еще может потребоваться для других строк.

Кроме того, диспетчер кучи может «кэшировать» эту виртуальную память для будущего использования, поэтому он вообще не будет ее возвращать, но будет готов к будущему выделению чего-либо.

В этом описании не учитываются многие детали, но их должно быть достаточно, чтобы понять эту проблему и возможные аналогичные проблемы. Будьте готовы это понять

  • в отладочной сборке и релизной сборке все работает немного по-другому.
  • с отладчиком все работает немного иначе, чем без отладчика.
  • в Linux все может быть по-другому
  • размеры, которые я упомянул здесь, могут быть разными, например. по архитектуре Itanium
  • Кучи управляются сегментами, а сегменты — блоками и т. д.

Так что да, это немного сложно. Просто поверьте C++, диспетчеру кучи и диспетчеру виртуальной памяти, что они поступают правильно. Обычно они это делают.

Спасибо за объяснение. На самом деле я использую Linux, извините, что вам пришлось догадываться, но я прочитал больше о том, что вы объяснили, и концепции в основном те же. Теперь я понимаю, что менеджер кучи может удерживать фрагменты памяти, даже если каждая память внутри них освобождена. Но мне кажется странным, что он может удерживать столько уже освобожденной памяти.

Zoraphie 25.09.2023 12:50

Другие вопросы по теме