Как уменьшить потребление памяти C++ по умолчанию?

У меня есть серверное приложение, написанное на C++. После запуска он использует около 480 КБ памяти на x86 Linux (Ubuntu 8.04, GCC 4.2.4). Я думаю, что 480 КБ — это избыточный объем памяти: сервер еще даже ничего не делает, к серверу не подключались клиенты. (См. также мой комментарий ниже, в котором я объясняю, почему я думаю, что 480 КБ — это много памяти.) Единственное, что делает сервер во время инициализации, — это порождает один или два потока, устанавливает несколько сокетов и другие простые вещи, которые не нужны. т очень интенсивное использование памяти.

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

После некоторого тестирования выяснилось, что что-то в среде выполнения C++ заставляет мой сервер использовать столько памяти, даже если сам сервер ничего не делает. Например, если я вставлю

getchar(); return 0;

сразу после

int main(int argc, char *argv[]) {

тогда использование памяти по-прежнему составляет 410 КБ на экземпляр!

Мое приложение зависит только от Curl и Boost. У меня достаточно опыта программирования на C, и я знаю, что библиотеки C не увеличивают потребление памяти, пока я их не использую.

Другие вещи, которые я нашел:

  • Простое приложение Hello World C потребляет около 50 КБ памяти.
  • Простое приложение hello world C, связанное с Curl, но не использующее Curl, также потребляет около 50 КБ памяти.
  • Простое приложение Hello World C++ (без Boost) потребляет около 100 КБ памяти.
  • Простое приложение Hello World C++, которое включает некоторые заголовки Boost, но фактически не использует Boost, потребляет около 100 КБ памяти. Нет символов Boost при проверке исполняемого файла с помощью «nm».

Поэтому мой вывод таков:

  1. Gcc отбрасывает неиспользуемые символы Boost.
  2. Если мое приложение использует Boost, то что-то в среде выполнения C++ (вероятно, динамический компоновщик) заставляет его использовать много памяти. Но что? Как узнать, что это за вещи и что с ними делать?

Я помню, как несколько лет назад в KDE обсуждались проблемы динамического компоновщика C++. В то время динамический компоновщик Linux C++ вызывал медленное время запуска в приложениях KDE C++ и большое потребление памяти. Насколько мне известно, с тех пор эти проблемы были исправлены в средах выполнения C++. Но может ли что-то подобное быть причиной чрезмерного потребления памяти, которое я вижу?

Ответы экспертов gcc/динамических ссылок приветствуются.

Для тех, кому интересно, рассматриваемый сервер является агентом регистрации Phusion Passenger: https://github.com/FooBarWidget/passenger/blob/master/ext/common/LoggingAgent/Main.cpp


person Hongli    schedule 14.11.2010    source источник
comment
boost не очень специфичен, это всего лишь библиотека в том смысле, что вы можете получить ее в одном zip-файле, на самом деле это огромный набор библиотек. Если все, что вы используете, это boost::shared_ptr, вы можете обойтись менее чем на 350 КБ дополнительно, поэтому любая попытка сократить ваши накладные расходы должна быть специфичной для того, что вы используете.   -  person Steve Jessop    schedule 14.11.2010
comment
Я не вижу проблемы в 480 КБ/экземпляр. Возможно, вам следует беспокоиться об использовании памяти в реальных случаях использования, а не когда она ничего не делает.   -  person Puppy    schedule 14.11.2010
comment
Я не уверен, что то, что вы видите, является реальной проблемой. Это правда, 480 КБ — это много оперативной памяти для использования приложения hello world, но современные системы не оптимизированы для максимально эффективной работы hello world. Они оптимизированы для запуска полезных приложений. Таким образом, более актуальным вопросом должно быть не то, как мне уменьшить «hello world», а то, что мое фактическое приложение использует слишком много памяти, и если да, то как я могу это уменьшить?   -  person Jeremy Friesner    schedule 14.11.2010
comment
Я бы также поместил это в папку «преждевременная оптимизация»…   -  person Paul Michalik    schedule 14.11.2010
comment
@Steve: В основном это просто shared_ptr, поток, функция и привязка.   -  person Hongli    schedule 14.11.2010
comment
@DeadMG, @Jeremy: Да, 480 КБ — это немного, но мое программное обеспечение должно работать на VPS с ограниченным объемом памяти, где может быть важен каждый мегабайт. Я также хочу похвастаться тем, что занимает меньше всего места среди всех программ этого класса. И дело в том, что я уже знаю, как оптимизировать обычное использование памяти, но теперь кажется, что среда выполнения использует постоянный объем дополнительной памяти, над которым у меня мало контроля.   -  person Hongli    schedule 14.11.2010
comment
@Hongli IIRC boost::bind - настоящая свинья. Вы динамически связываетесь с boost и C++ stdlib? Кроме того, посмотрите листинг asm и посмотрите, сколько места занимает обработка исключений. Это может привести к раздуванию вашего стека. Я не уверен в Linux, но обработка исключений в Windows может использовать МНОГО стека.   -  person JimR    schedule 14.11.2010
comment
@JimR: я ссылаюсь только на boost-thread, все остальное, что я использую, — это только заголовок. Но как boost использует пространство стека даже до того, как я вызову какие-либо функции boost?   -  person Hongli    schedule 14.11.2010
comment
@Hongli: если эти 480 КБ являются постоянными накладными расходами, в чем именно проблема? В любом случае, попробуйте статически связать используемые вами библиотеки (и, если возможно, со средой выполнения C++). Но в основном я думаю, что это преждевременная оптимизация, и не только потому, что 500 КБ не имеют значения, но и потому, что в более крупном приложении нет разницы. Память, используемая заранее, может избавить приложение от необходимости выделять дополнительные 480 КБ позже. Вы не знаете, что это накладные расходы в реальном приложении вообще.   -  person jalf    schedule 15.11.2010
comment
@Hongli: Но использует ли ваше приложение слишком много памяти в этом сценарии? Вы не протестировали реальные варианты использования.   -  person Puppy    schedule 15.11.2010
comment
@DeadMG: Это зависит от количества клиентов (использование памяти линейно увеличивается с количеством клиентов). Однако фактическое использование памяти сценария не имеет значения в этом вопросе; в этом вопросе я просто хочу знать, как минимизировать постоянное количество грязной памяти, налагаемой средой выполнения C++, я уже знаю, как справиться с остальным, не консультируясь со StackOverflow.   -  person Hongli    schedule 15.11.2010
comment
@Hongli: Как вы можете продемонстрировать, что существующее использование памяти не является просто предварительным выделением? Объем памяти, потребляемый hello, world, является действительно незначительным и неуместным обсуждением.   -  person Puppy    schedule 15.11.2010
comment
@DeadMG: я не могу продемонстрировать, что это не предварительное распределение, поэтому я и спрашиваю. Объем памяти, потребляемый hello world важен для меня, иначе я бы не спрашивал!   -  person Hongli    schedule 15.11.2010
comment
@Хогли: это? Почему? Если это предварительное распределение ЕСТЬ, то оно полностью и совершенно бессмысленно, а если вы не знаете, что это не так, то как именно оно актуально?   -  person Puppy    schedule 15.11.2010
comment
@DeadMG: Это актуально именно потому, что я не знаю. Если я не могу вернуть эти 400 КБ, то, по крайней мере, я хочу знать, откуда они взялись. Если что-то предварительно выделяется, то что предварительно выделяется и почему? Если бы я не хотел разбираться в низкоуровневых вещах, я бы не использовал C++.   -  person Hongli    schedule 16.11.2010


Ответы (3)


Среда выполнения C выделяет больше памяти, чем ваш процесс фактически использует в ходе нормальной работы. Это связано с тем, что выделение памяти на уровне ядра происходит очень медленно и может выполняться только блоками размером со страницу (размер страницы обычно составляет 4 КБ на компьютерах с архитектурой x86, но может быть больше и обычно составляет 8 КБ или более на машинах с архитектурой x64).

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

Наконец, если вы используете улучшения для повышения, они, вероятно, зависят от некоторых компонентов STL, таких как std::vector. Эти компоненты выделяют пространство для элементов с помощью std::allocator<T>, что в некоторых случаях снова выделяет больше места, чем фактически используется. (В частности, структуры на основе узлов, такие как std::map, std::set и std::list, обычно делают это, чтобы поместить узлы списка или дерева вместе на одну и ту же страницу памяти)

Короче говоря: не беспокойтесь об этом. Половина мегабайта памяти — это не так уж и много (по крайней мере, в настоящее время), и большая часть этого, вероятно, просто амортизирует использование функций динамического распределения. Напишите свой фактический сервер, и если он использует слишком много памяти, ТОГДА поищите способы уменьшить использование памяти.

РЕДАКТИРОВАТЬ: Если компонент boost, который вы используете, является asio, и вы используете сокеты, вы также должны знать, что для поддержки буферов для сокетов также требуется некоторая память.

person Billy ONeal    schedule 14.11.2010
comment
Эти буферы не должны выделяться, если сокеты фактически не созданы. Его программа int main() { return getchar(); } не должна включать в себя буфеты сокетов (если только у него нет глобальных переменных). - person Ben Voigt; 14.11.2010
comment
Я знаю, что malloc() предварительно выделяет память. Но, как я уже продемонстрировал, память увеличивается еще до того, как мое приложение вообще вызывает malloc(). - person Hongli; 14.11.2010
comment
@Ben: у меня нет глобальных переменных, которые вызывают выделение кучи. - person Hongli; 14.11.2010
comment
@Ben: Его int main() { return getchar(); } тоже не занимает 400 КБ памяти. @Hongli: Вы уверены? Вы прошли через все вызовы повышения, которые вы использовали, и не было вызовов каких-либо функций динамического распределения? - person Billy ONeal; 14.11.2010
comment
Я только что еще раз вручную проверил все мои исходные файлы. Есть около 3 глобальных переменных с конструктором: пустой std::list, boost::mutex и std::list::iterator. Ни один из них не должен выделять память в куче или, по крайней мере, должен выделять очень мало. Я не использую асио. - person Hongli; 15.11.2010
comment
@Hongli: Я думаю, это std::list. В любом случае вам вероятно следует использовать vector. - person Billy ONeal; 15.11.2010
comment
Я думаю, что std::list изначально выделил бы даже меньше, чем std::vector. - person Ben Voigt; 15.11.2010
comment
@Billy: Согласно OP, его int main() { return getchar(); } занимает 410 КБ. не совсем 480 КБ, используемые полным кодом, но очень существенный кусок. - person Ben Voigt; 15.11.2010
comment
@Ben: Обычно std::vector ничего не выделяет при пустой конструкции. OYOH, std::list обычно имеет один из настраиваемых распределителей, о которых я говорил, для кластеризации узлов списка на одной странице памяти. И я полагаю, что вы неправильно поняли вопрос - он конкретно говорит, что простой основной, как указано, занимает 100 КБ. Только после того, как он А. помещает глобальные переменные и Б. ссылки на буст, использование памяти программой увеличивается. В любом случае, это действительно не имеет значения. - person Billy ONeal; 15.11.2010
comment
@Ben: в моем тесте список все время пуст. Во всяком случае, я попытался удалить список global; это сэкономило мне только 1 КБ грязного привата. - person Hongli; 15.11.2010
comment
@Billy: Похоже, что загрузка библиотеки boost (.so) увеличивает использование памяти. Это должно быть несколько дополнительных сотен КБ, совместно используемых всеми процессами, использующими библиотеку, а не 300 КБ частной фиксации для каждого процесса. - person Ben Voigt; 15.11.2010

Один из способов уменьшить потребление памяти — уменьшить размер стека потоков.

Что касается ускорения, как прокомментировал Стив Джессоп, вы должны быть более конкретными, чем «ускорение».

person ronag    schedule 14.11.2010
comment
Это повлияет на использование виртуального адресного пространства, но не должно повлиять на частную фиксацию. - person Ben Voigt; 14.11.2010
comment
@Ben Voigt Это зависит от того, сколько стека вы сказали системе зафиксировать изначально. Уменьшите размер стека и размер начального коммита. - person JimR; 14.11.2010

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

Повторно запустите prelink во всей системе. Если библиотека загружается по предпочитаемому адресу, она будет отображена как разделяемая память и будет стоить только одну копию кода, независимо от того, сколько процессов ее использует.

Кстати, prelink также было исправлением для KDE.

person Ben Voigt    schedule 14.11.2010
comment
Где я могу узнать больше о конфликтах базовых адресов? И разве вы не имеете в виду prelink вместо preload? - person Hongli; 14.11.2010
comment
Эквивалентом Windows является BIND. - person MSalters; 15.11.2010
comment
Эквивалент cygwin — rebase, а для собственных приложений Windows он встроен в инструменты SDK, вы можете указать базовый адрес во время компоновки при создании DLL или позже с помощью editbin. Windows на самом деле не выполняет предварительную компоновку, но получение предпочтительного базового адреса или нет по-прежнему является разницей между общим разделом и частной копией образа после исправления. - person Ben Voigt; 15.11.2010
comment
Я думал, что только Windows загрязняет память при перемещении динамических библиотек в пользу скорости процессора, и что системы ELF сделали явный выбор для экономии памяти вместо процессора с помощью кода PIC. Думаю, мне нужно еще раз взглянуть на эту тему. - person Hongli; 16.11.2010
comment
@Hongli: Удар может быть меньше с PIC (-fPIE), но все же потребуется некоторое исправление. Возможно, в коде, связанном с динамической библиотекой, если не в самом .so. Во всяком случае, ELF не требует кода PIC, вы уверены, что -fPIE использовался во время компиляции (вашего приложения и общей библиотеки)? - person Ben Voigt; 16.11.2010
comment
Отличается ли -fPIE от -fPIC? Мое приложение не скомпилировано с -fPIC/-fPIE, но все общие библиотеки компилируются. Я думал, что с PIC нужно компилировать только общие библиотеки. - person Hongli; 18.11.2010
comment
Совместно используемые библиотеки не нужно компилировать с параметром -fPIC, но это предотвращает перемещения внутри кодовых страниц (я предполагаю, что где-то все еще есть несколько переменных, которые необходимо установить так, чтобы они указывали на базовый адрес библиотеки, но только на одну страницу вместо весь раздел кода). См. sourceware.org/ml/glibc-linux/2000-q2/msg00067. .html и особенно команда objdump --dynamic-reloc foo.so - person Ben Voigt; 18.11.2010