LINUX.ORG.RU

Отложенное освобождение памяти

 , ,


3

3

Как известно, в стандартной библиотеке C++ есть умный указатель с подсчётом ссылок std::shared_ptr; при желании можно думать о любом другом указателе с подсчётом ссылок, смысл дальнейшего от этого не изменится.

Как известно, при достижении счётчиком ссылок нуля вызывается deleter для указателя, управляемого shared_ptr-ом. По-умолчанию deleter просто применят оператор delete к указателю.

На что я хочу обратить внимание: работу по вызову деструктора и освобождению памяти делает тот тред и тот код, который сбрасывает счётчик до нуля. Если объект содержит другие shared_ptr в качестве своих полей, то часто освобождение этого объекта вызывает каскад освобождений памяти и приводит к задержкам в выполнении треда. Пример кода, могущий привести к таким каскадным высвобождениям, можно найти, например, тут https://bartoszmilewski.com/2013/11/13/functional-data-structures-in-c-lists/. Там односвязные иммутабельные списки, для предотвращения копирования всего списка при модификации, например, только головы, реализованы с использованием shared_ptr и могут иметь общие хвосты. Короче, с помощью shared_ptr реализуется persistence, я думаю вы знакомы с таким подходом.

Я тут подумал и пришёл к такой идее: завести threadsafe очередь для указателей (точнее, для структур, содержащих указатель + указатель на функцию, знающую что с этим указателем делать, ведь нам придётся стереть типы; но это уже детали) и при создании shared_ptr использовать custom deleter, который при вызове будет просто помещать указатель в очередь. Вызывать же деструкторы и освобождать память будет отдельный поток (или потоки?). Он будет брать очередной указатель из очереди и вызывать для него деструктор и освобождать память. Так мы избавим рабочие потоки от необходимости обслуживать каскады высвобождений памяти.

Я понимаю, что у этого подхода тоже будут performance penalties. Обычно куч всего несколько и при большом числе тредов каждая обслуживает несколько тредов. И если тред-освободитель будет освобождать память, он захватит лок у кучи, в которую могут лезть треды для выделения памяти. Там-то они и будут сталкиваться лбами. Это я понимаю. Но, в отличие от гарантированных длинных задержек, вызванных каскадным высвобождением памяти, тут задержки будут размазаны во времени или будут «распределены» между другими тредами, если они полезут выделять память в момент освобождения; или же, очень вероятно, эти задержки вообще не проявятся, если память выделять не полезут. Надо тестировать под различными нагрузками, заранее трудно сказать.

А что ЛОР про это думает? Дискас.



Последнее исправление: dependent_type (всего исправлений: 2)
Ответ на: комментарий от dave

Ну, тогда оно всё-таки сливает tcmalloc(недавно проверяли). Чистый результат не скажу, т.к. замерялись не сами аллокации/деаллокации а раундтрип целевых сообщений. Он улучшился на 20-25% с tcmalloc.

pon4ik ★★★★★
()
Ответ на: комментарий от DarkEld3r

Будет ли это GC, учитывая, что в библиотеке могут быть баги?

Я указал выше два фактора, которые не позволяют считать shared_ptr сборщиком мусора с практической точки зрения. Если кто-то предоставляет библиотеку, которая решает проблему сборки мусора с учетом этих двух факторов, то это будет GC. Даже если в ней будут баги.

Мой спор с tailgunner-ом состоит в том, что shared_ptr он относит к GC исключительно по формальным признакам (которые сами по себе допускают широкое толкование).

eao197 ★★★★★
()
Ответ на: комментарий от pon4ik

У нас большая нагрузка на стек TCP/IP была, да на вывод в файлы. Поэтому я разницы, возможно, и не заметил. Вполне допускаю, что tcmalloc эффективнее стандартного на некоторых задачах

dave ★★★★★
()
Ответ на: комментарий от kawaii_neko

Я, конечно, не спец в плюсах, но ты вручную выделил память под shared_ptr, а подчистить забыл.
Косяк твой, что ты хотел этим сказать?

mersinvald ★★★★★
()
Ответ на: комментарий от dave

У нас тоже нагрузка на tcp/ip стек была (но там IB сетевуха может там по другому что-то работает, стек был именно net а не ib), а по памяти характер 1к-10к обьектов в период 40мкс создаётся и хотя бы раз читается и половина из них помирает в период 1-2ms.

Есно это всё в цикле. Иногда помирают почти все. Некоторые живут оочень долго. Т.е. по факту ситуация с фрагментацией должна быть на лицо.

pon4ik ★★★★★
()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от eao197

Ты хотел, чтобы я чем-то «подтвердил хоть чем-то»? Я привел общепринятые определения. Если у тебя другие, то нет предмета спора.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Reference counting is a form of garbage collection whereby each object has a count of the number of references to it

А shared_ptr повсюду и разыменовывания в нужных местах тоже конпелятор припишет?

В Perl5 и Python 1.x не справлялась. Тем не менее, все называли это сборкой мусора - хотя, конечно, ты можешь назвать это как сам захочешь.

Регулярно сталкиваюсь с перлопитоноподелиями, текущими со скоростью несколько мегабайт в минуту. Это говнокодеры или «сборщеги мусора» не справляются с циклическими ссылками?

kawaii_neko ★★★★
()
Ответ на: комментарий от tailgunner

Следуя твоей логике получается так: shared_ptr — это GC => т.к. shared_ptr является частью языка, то C++ — это язык с GC. Что, очевидно, противоречит окружающей меня реальности.

Поэтому я тебе и говорю, что GC на базе подсчета ссылок — это возможно. Но не каждый подсчет ссылок — это GC, что и имеет место быть в случае shared_ptr. И это так же не противоречит твоим цитатам.

Вот я тебя и прошу в который раз пояснить, каким боком shared_ptr соотносится с GC?

eao197 ★★★★★
()
Ответ на: комментарий от kawaii_neko

А shared_ptr повсюду и разыменовывания в нужных местах тоже конпелятор припишет?

Как там бузина в огороде?

Регулярно сталкиваюсь с перлопитоноподелиями, текущими со скоростью несколько мегабайт в минуту. Это говнокодеры или «сборщеги мусора» не справляются с циклическими ссылками?

Если ты не можешь ничего с этим сделать (а ты не можешь) - какая тебе разница.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Ну и поясни две вещи:

- почему ты тогда говоришь, что ручной подсчет ссылок в ядре не является GC?

- является ли C++ языком с GC?

eao197 ★★★★★
()
Ответ на: комментарий от tailgunner

Как там бузина в огороде?

То есть, сказать нечего?

Если ты не можешь ничего с этим сделать (а ты не можешь) - какая тебе разница.

Снова нечего сказать по существу?

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Как там бузина в огороде?

То есть, сказать нечего?

То есть ты спросил какую-то нерелевантную чушь.

Снова нечего сказать по существу?

Ты у меня спрашиваешь. почему растет объем памяти неназванных приложений, которые ты используешь? Да ты совсем поехал.

tailgunner ★★★★★
()
Ответ на: комментарий от eao197

почему ты тогда говоришь, что ручной подсчет ссылок в ядре не является GC?

Потому что он ручной.

является ли C++ языком с GC?

Нет. Си++ является языком с библиотечной реализацией GC, основанного на подсчете ссылок.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Потому что он ручной.

Ага, а в C++ shared_ptr появляется автоматически. И за временем жизни shared_ptr разработчик следить не должен. И ошибки, вроде вот таких:

auto make_callback() {
  auto s = std::make_shared<some_data>();
  return [&](some_arg arg) { s->handle(arg); };
}
невозможны.

Не говоря уже про вырассмотренные циклические ссылки.

Нет. Си++ является языком с библиотечной реализацией GC, основанного на подсчете ссылок.

Если бы под «библиотечной реализацией» понимался Боем GC, то еще куда не шло. Но вот называть shared_ptr GC можно только от большой любви к формализму.

eao197 ★★★★★
()
Ответ на: комментарий от tailgunner

То есть ты спросил какую-то нерелевантную чушь.

Если захочется чуши, можешь почитать себя в этом треде: «shared_ptr — это gc, потому что конпелятор вызывает деструкторы сам. А в ядре нет gc, потому что там нужно инкрементить/декрементить ссылки вручную».

Ты у меня спрашиваешь. почему растет объем памяти неназванных приложений, которые ты используешь? Да ты совсем поехал.

Я не спрашиваю «почему». Я предложил два варианта. Судя по ответу, ты сомневаешься, что в перлопистонах gc управляются с циклически связанными объектами.

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Я не спрашиваю «почему». Я предложил два варианта.

Это всё меняет.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Си++ является языком с библиотечной реализацией GC, основанного на подсчете ссылок.

Передёргиваешь, путём смешения и подмены понятий.

Подсчёт ссылок - одна из техник для реализации GC.

Но сам по себе GC - это не зависящий от разработчика и не управляемый им напрямую, либо управляемый очень ограниченно и косвенно кусок кода.

Крестовые умноуказатели не доросли до столь почётного титула.

LamerOk ★★★★★
()
Ответ на: комментарий от LamerOk

Крестовые умноуказатели не доросли до столь почётного титула.

Наоборот же, не опустились до презренных коcтылей.

mashina ★★★★★
()
Ответ на: комментарий от tailgunner

Масшатб не тот.

GC либо не управляется программистом вообще, либо управляется в объёме «GC.collect();».

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

LamerOk ★★★★★
()
Ответ на: комментарий от tailgunner

Ты не понял. В C++ точно такой же ручной подсчет ссылок, как и в C, только делается он не вызовом inc/dec, а через копирование экземпляров. При этом разработчик должен понимать, что это копирование нужно. Если же он вместо копирования оперирует ссылками (как это и происходит в показанном примере), то никаких inc/dec автоматическим образом не возникнет. Так что те же яйца, только в профиль. Но в C у тебя GC на подсчете ссылок нет, а в C++ — есть.

eao197 ★★★★★
()
Ответ на: комментарий от LamerOk

Масшатб не тот.

Недостаточно всеобъемлюще?

GC либо не управляется программистом вообще, либо управляется в объёме «GC.collect();».

shared_ptr примерно так и управляется.

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

Процесса ОС? Так в Erlang тоже нет GC для приложения или процесса, но никто не спорит, что в Erlang есть GC.

tailgunner ★★★★★
()
Ответ на: комментарий от dependent_type

И scope-based memory management — тоже GC.

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

GC - это то, как управляет памятью VM типа Java, Go или .NET, а то, что тут описано - это ручное управление памятью (или полуручное, как в std::shared_ptr).

chimera19
()
Ответ на: комментарий от chimera19

GC - это то, как управляет памятью VM типа Java, Go или .NET

Но ведь у Go нет VM.

tailgunner ★★★★★
()
Ответ на: комментарий от eao197

Ты не понял.

А по-моему, это ты не понял.

В C++ точно такой же ручной подсчет ссылок, как и в C, только делается он не вызовом inc/dec, а через копирование экземпляров.

То же самое можно сказать про любую систему на reference counting.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

«The garbage collector, or just collector, attempts to reclaim garbage, or memory occupied by objects that are no longer in use by the program.»

Приведи способ управления памятью, который не подпадает под это определение.

chimera19
()
Ответ на: комментарий от tailgunner

По твоему free() не «пытается возвратить память, занятую объектами, которые больше не используются программой»?

chimera19
()
Ответ на: комментарий от tailgunner

То же самое можно сказать про любую систему на reference counting.

Если вызовы inc/dec генерируются компилятором, а не вписываются разработчиком, то не про любую.

eao197 ★★★★★
()
Ответ на: комментарий от chimera19

По твоему free() не «пытается возвратить память, занятую объектами, которые больше не используются программой»?

Нет. Он ее освобождает. Безусловно и не заботясь о том, используется ли она.

tailgunner ★★★★★
()
Ответ на: комментарий от eao197

Если вызовы inc/dec генерируются компилятором, а не вписываются разработчиком, то не про любую.

Если ты утверждаешь, что в случае shared_ptr вызовы inc/dec вписываются разработчиком, на этом можно и закончить.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Если ты утверждаешь, что в случае shared_ptr вызовы inc/dec вписываются разработчиком, на этом можно и закончить.

Я утверждаю, что в случае shared_ptr разрабочик вынужден писать так:

auto make_callback() {
  auto s = std::make_shared<some_data>();
  return [s](some_arg arg) { s->handle(arg); };
}
а не вот так:
auto make_callback() {
  auto s = std::make_shared<some_data>();
  return [&s](some_arg arg) { s->handle(arg); };
}
как раз потому, что в языке нет GC и компилятор не может поставить inc/dec автоматически.

eao197 ★★★★★
()
Ответ на: комментарий от tailgunner

Недостаточно всеобъемлюще?

Именно.

GC - это кусок программного кода, отвечающего за выделение и переиспользование произвольного (0-N) кол-ва диапазонов адресов для объектов произвольной(0-M) размерности и отслеживающий *для этих целей* время жизни выделенных диапазонов адресов.

Есть код, реализующий логику в таком объёме - есть GC. Нет такой функциональности - нет и GC.

GC включает в себя полноценный аллокатор памяти.

LamerOk ★★★★★
()
Ответ на: комментарий от dave

То что подсчет ссылок к GC относится, мне встречалось по крайней мере в трех авторитетных источниках (не считая «статейки»), обратное - ни разу. Поделитесь?

match
()
Ответ на: комментарий от match

Можно, конечно, издеваться над здравым смыслом, но зачем? Ну, ладно, в терминологическом споре участвовать не хочу. Я не такой упертый, и у меня нет столько времени

dave ★★★★★
()
Ответ на: комментарий от LamerOk

GC либо не управляется программистом вообще, либо управляется в объёме «GC.collect();».

В том же .Net есть побольше возможностей для «настройки», в том числе, разные KeepAlive(Object).

DarkEld3r ★★★★★
()
Ответ на: комментарий от eao197

Я утверждаю, что в случае shared_ptr разрабочик вынужден писать так:

Дык, с честным GC тоже можно в ногу выстрелить: передаём указатель на объект в неуправляемый код не позаботившись запретить его перемещение/удаление и готово. Немного сложнее, но тут скорее (большая) низкоуровневость плюсов сказывается, а смысл примерно такой же.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Дык, с честным GC тоже можно в ногу выстрелить

Поясню еще раз: при работе с shared_ptr разработчик точно так же должен помнить про inc/dec (и заботиться о том, чтобы inc/dec выполнялись), как и в случае с ручными вызовами inc/dec. Только для tailgunner-а shared_ptr — это GC, а ручной вызов inc/dec — нет.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Поясню еще раз:

Поясню ещё раз: я тоже разделяю GC и shared_ptr просто потому, что так удобнее и проще находить общий язык. Но большинство приведённых аргументов в пользу того, что shared_ptr не является GC кажутся несколько притянутыми за уши. Про особенности (обычного/нормального) GC тоже местами надо помнить и иногда тоже приходится принимать дополнительные действия, чтобы всё работало как надо.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Но большинство приведённых аргументов в пользу того, что shared_ptr не является GC кажутся несколько притянутыми за уши.

Ну OK. Если нет аргументов, значит shared_ptr, согласно определению GC из Wikipedia, является GC, а C++ — является языком с GC.

Надеюсь, вам с tailgunner-ом такая точка зрения помогает на практике.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Надеюсь, вам с tailgunner-ом такая точка зрения помогает на практике.

Полезно знать, что использованием shared_ptr подписываешься на все проблемы сборки мусора.

tailgunner ★★★★★
()
Ответ на: комментарий от eao197

Надеюсь, вам с tailgunner-ом такая точка зрения помогает на практике.

Я ведь уточнил, что «на практике» как раз разделяю GC и shared_ptr. (:

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

Полезно знать, что использованием shared_ptr подписываешься на все проблемы сборки мусора.

Всё-таки у shared_ptr с «предсказуемостью», если не брать особо запущенные случаи, несколько лучше.

DarkEld3r ★★★★★
()
Ответ на: комментарий от eao197

Когда теория расходится с практикой, то где-то что-то не так.

Я бы мог отделаться цитатой про теорию и практику, но уточню: удобство разделения проявляется хотя бы в отсутствии необходимости каждый раз уточнять «GC на отдельных типах с подчётом ссылок» или «нормальный GC». Ну и все (?) современные языки с GC умеют обрабатывать циклически ссылки.

DarkEld3r ★★★★★
()
Ответ на: комментарий от eao197

Тоже хотел так написать. Но главное преимущество GC - автоматическое освобождение памяти, а его как раз получаешь.

tailgunner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.