С++ выделяет аномально большую память amout для переменных

Недавно я узнал, что целое число занимает 4 байта из памяти.

Сначала запустил этот код и измерил использование памяти:

int main()
{
   int *pointer;
}

enter image description here

  • Требуется 144 КБ.

Затем я изменил код, чтобы выделить 1000 целых переменных.

int main()
{
   int *pointer;

   for (int n=0; n < 1000; n++)
     { 
       pointer = new int ; 
     }
}

enter image description here

  • Затем потребовалось (168-144 =) 24 КБ
    , но предполагается, что 1000 целых чисел занимают (4 байта x 1000 =) 3,9 КБ.

Затем я решил сделать 262,144 целых переменных, которые должны потреблять 1 МБ памяти.

int main()
{
   int *pointer;

   for (int n=0; n < 262144; n++)
     { 
       pointer = new int ; 
     }
}

Удивительно, теперь требуется 8 МБ

enter image description here

Использование памяти, экспоненциально растет в соответствии с количеством целых чисел.
Почему это происходит?

Я нахожусь на Kubuntu 13.04 (amd64)
Пожалуйста, дайте мне небольшое объяснение. Спасибо!

ПРИМЕЧАНИЕ: sizeof(integer) возвращает 4

Ответ 1

Память для отдельных распределенных динамических объектов не обязательна. Фактически, из-за требований к выравниванию для new char[N] (а именно для выравнивания в alignof(std::maxalign_t), который обычно равен 16), стандартный распределитель памяти может просто никогда не возвращать ничего, кроме 16-байтовой выровненной памяти. Поэтому каждое распределение int фактически потребляет (не менее) 16 байт. (И дополнительная память может потребоваться распределителем для внутренней бухгалтерии.)

Мораль, конечно, должна использоваться std::vector<int>(1000000), чтобы получить разумный дескриптор на миллион динамических целых чисел.

Ответ 2

Неоптимизированные распределения в общих распределителях идут с некоторыми накладными расходами. Вы можете представить себе два "блока": блок INFO и STORAGE. Блок информации, скорее всего, будет прямо перед блоком хранения.

Итак, если вы выделите, у вас будет что-то подобное в вашей памяти:

        Memory that is actually accessible
        vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
--------------------------------------------
|  INFO |  STORAGE                         |
--------------------------------------------
^^^^^^^^^
Some informations on the size of the "STORAGE" chunk etc.

Кроме того, блок будет выровнен по определенной степени детализации (примерно как 16 байтов в случае int).

Я напишу о том, как это выглядит на MSVC12, так как я могу проверить это на данный момент.

Давайте посмотрим на нашу память. Стрелки указывают 16 байтовых границ.

Memory layout in case of int allocation; each block represents 4 bytes

Если вы выделите одно целое число из 4 байтов, вы получите 4 байта памяти на границе с 16 байтами (оранжевый квадрат после второй границы). 16 байтов, предваряющих этот блок (синие), заняты для хранения дополнительной информации. (Я пропущу такие вещи, как endianess и т.д., Но имейте в виду, что это может повлиять на этот вид макета.) Если вы прочитаете первые четыре байта этого 16-байтового блока перед вашей выделенной памятью, вы найдете количество выделенных байтов.

Если вы теперь выделите второе 4-байтовое целое число (зеленая рамка), то его позиция будет по крайней мере на два раза меньше, чем на 16 байт, так как перед ним должен встать блок INFO (желтый/красный), что не соответствует правая нижняя граница. Красный блок снова является тем, который содержит количество байтов.

Как вы можете легко увидеть: если зеленый блок был бы на 16 байт раньше, красный и оранжевый блок перекрывались бы - невозможно.

Вы можете проверить это для себя. Я использую MSVC 2012, и это сработало для меня:

char * mem = new char[4096];
cout << "Number of allocated bytes for mem is: " << *(unsigned int*)(mem-16) << endl;
delete [] mem;


double * dmem = new double[4096];
cout << "Number of allocated bytes for dmem is: " << *(unsigned int*)(((char*)dmem)-16) << endl;
delete [] dmem;

печатает

Number of allocated bytes for mem is: 4096
Number of allocated bytes for dmem is: 32768

И это совершенно правильно. Поэтому выделение памяти с использованием new имеет в случае MSVC12 дополнительный блок "INFO" размером не менее 16 байтов.

Ответ 3

Вы выделяете несколько динамических переменных. Каждая переменная содержит 4 байта данных, но диспетчер памяти обычно хранит некоторую дополнительную информацию о выделенных блоках, и каждый блок должен быть выровнен, что создает дополнительные служебные данные.

Попробуйте pointer = new int[262144]; и посмотрите разницу.

Ответ 4

Каждый раз, когда вы выделяете int:

  • Распределитель памяти должен предоставить вам 16-байтовый выровненный блок пространства, поскольку в общем случае распределение памяти должно обеспечивать соответствующее выравнивание, чтобы память могла использоваться для любых целей. Из-за этого каждое распределение обычно возвращает не менее 16 байт, даже если запрашивается меньше. (Требования к выравниванию могут варьироваться в зависимости от системы и системы. И возможно, что небольшие распределения могут быть оптимизированы для использования меньшего пространства. Однако опытные программисты знают, чтобы избежать множества небольших распределений.)
  • Распределитель памяти должен использовать некоторую память, чтобы помнить, сколько места было выделено, чтобы он знал, сколько существует, когда вызывается free. (Это может быть оптимизировано путем объединения знаний о пространстве, используемом с оператором delete. Однако общие процедуры распределения памяти обычно отделены от компиляторов new и delete.)
  • Распределитель памяти должен использовать некоторую память для структур данных для организации информации о выделенных и освобожденных блоках памяти. Возможно, для этих структур данных требуется пространство O (n • log n), так что накладные расходы растут, когда имеется много небольших распределений.

Другим возможным эффектом является то, что, по мере использования памяти, распределитель запрашивает и инициализирует большие куски из системы. Возможно, в первый раз, когда вы используете исходный пул памяти, распределитель запрашивает еще 16 страниц. В следующий раз он запросит 32. В следующий раз 64. Мы не знаем, сколько памяти, которую запросил система распределения памяти из системы, действительно использовалось для удовлетворения ваших запросов для объектов int.

Не динамически выделять много мелких объектов. Вместо этого используйте массив.

Ответ 5

Я думаю, это зависит от того, как компилятор создает выходную программу.

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

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

Ответ 6

Два объяснения:

  • Динамическое распределение памяти (в куче) не обязательно смежное. При использовании new выполняется динамическое распределение.
  • Если вы включаете символы отладки (-g флаг компилятора), использование вашей памяти может быть больше, чем ожидалось.

Ответ 7

Каждое объявление делает новую переменную подходящей для параметров выравнивания компилятора, которым нужны пробелы между (начальный адрес переменной должен быть кратным 128 или 64 или 32 (битам), и это приводит к неэффективности для памяти многих переменных против одного массива). Массив гораздо полезнее иметь смежную область.

Ответ 8

Вы выделяете больше, чем просто int, вы также выделяете блок кучи, который имеет накладные расходы (который зависит от платформы). Что-то нужно отслеживать информацию о куче. Если вместо этого вы выделили массив ints, вы увидите, что использование памяти больше соответствует вашим ожиданиям.

Ответ 9

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

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

Попробуйте запустить программу под strace и найдите вызовы на brk и обратите внимание на то, насколько большой запрос каждый раз.