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. Обычно куч всего несколько и при большом числе тредов каждая обслуживает несколько тредов. И если тред-освободитель будет освобождать память, он захватит лок у кучи, в которую могут лезть треды для выделения памяти. Там-то они и будут сталкиваться лбами. Это я понимаю. Но, в отличие от гарантированных длинных задержек, вызванных каскадным высвобождением памяти, тут задержки будут размазаны во времени или будут «распределены» между другими тредами, если они полезут выделять память в момент освобождения; или же, очень вероятно, эти задержки вообще не проявятся, если память выделять не полезут. Надо тестировать под различными нагрузками, заранее трудно сказать.

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

Попробуй для начала tcmalloc

pon4ik ★★★★★ ()

Вообще «отложенное освобождение памяти» - это GC

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

Иногда складывается такое ощущение, что у тебя всегда под рукой «Справочник Единственно-Верных Толкований Терминов™».

i-rinat ★★★★★ ()

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

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

Это, кстати, может быть полезно для работы с большими кусками памяти для которых malloc(...) и free(...) превращаются в тяжёлые mmap(...) и munmap(...) или если объект в принципе имеет тяжёлый деструктор с какой-то логикой.

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

Это мнение мне пока не встречалось. Я склонен считать его неверным.

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

Я дал клятву не показывать эту книгу целиком, но вот одна глава из нее.

tailgunner ★★★★★ ()

То, что ТС отписался от собственного треда, символизирует.

Можно не продолжать.

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

Ну спасибо. Теперь в моём списке к прочтению не 300, а 301 ссылка.

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

И reference counting - тоже GC.

А луна - это солнце. Нет, все же подсчет ссылок к GC не относится. Скорее, полуавтоматическая система управления памятью

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

все же подсчет ссылок к GC не относится

Не относится кем именно? Ссылка выше.

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

О, да! Кто-то статейку написал, и давайте теперь синее называть голубым

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

По ссылке выше перечень возможных принципов работы GC, в том числе и на базе подсчета ссылок. Тебе осталось показать, что shared_ptr имеют к этим GC отношение.

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

Тебе осталось показать, что shared_ptr имеют к этим GC отношение.

Зачем и к каким «этим»? Я говорил о подсчете ссылок как о технике. Но shared_ptr - тоже сборка мусора.

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

Я говорил о подсчете ссылок как о технике.

Я понял.

Но shared_ptr - тоже сборка мусора.

Ну вот и не очевидно, что shared_ptr — это сборка мусора. Ибо:

- сборка мусора не требует явного участия программиста. Если GC работает на основе подсчета ссылок, то инкременты/декременты должен расставлять в нужных местах сам компилятор. В случае же shared_ptr все делается вручную (посредством явного декларирования ссылок через shared_ptr);

- сборка мусора должна справляться с циклами в ссылках без участия программиста. Иначе от «сборки мусора» остается только название. В случае же с shared_ptr разруливание циклических ссылок лежит на программисте. Т.е. имеет место именно что ручная работа с памятью.

Оба эти фактора не позволяют с практической точки зрения считать shared_ptr механизмом сборки мусора. Хотя, если троллить оппонентов посредством банальной эрудиции, то можно и shared_ptr сборкой мусора обозвать.

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

сборка мусора не требует явного участия программиста. Если GC работает на основе подсчета ссылок, то инкременты/декременты должен расставлять в нужных местах сам компилятор.

Инкременты и декременты? Их и расставляет компилятор.

В случае же shared_ptr все делается вручную (посредством явного декларирования ссылок через shared_ptr);

Garbage-collected ссылка объявляется вручную, да. Потому что есть выбор - использовать GC или нет. Принципиально это не отличается от двух видов ссылок в C++/CLI или даже @nogc в D.

сборка мусора должна справляться с циклами в ссылках без участия программиста

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

Хотя, если троллить оппонентов посредством банальной эрудиции

Нужно напоминать людям о простых вещах.

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

shared_ptr - тоже сборка мусора

void i_leak_mem()
{
  auto *ptr = new shared_ptr<int>(new 1);
}

Удачной «сборки мусора» бгг.

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

Их и расставляет компилятор.

Компилятор вызывает конструкторы, деструкторы и операторы копирования-перемещения. И все. Остальное нужно ручками вписывать. Что и сделано в коде shared_ptr.

Принципиально это не отличается от двух видов ссылок в C++/CLI или даже @nogc в D.

Это разные вещи: запрет GC обрабатывать ссылки определенного вида и ручное управление памятью посредством подсчета ссылок.

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

Но ведь там разработчику не приходилось писать shared_ptr<MyObject> вместо MyObject. Так что там хотя бы одно из описанных мной выше условий выполнялось. В случае с shared_ptr не выполняются оба.

Так что ты пока еще не доказал, что shared_ptr — это сборщик мусора.

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

Почему тебя это волнует? Ты отписался от топика.

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

Компилятор вызывает конструкторы, деструкторы и операторы копирования-перемещения. И все.

Нет разницы между «компилятор вызывает функции инкремента/декремента» и «компилятор вызывает конструкторы, деструкторы и операторы копирования-перемещения, которые выполняют инкремент/декремент».

Принципиально это не отличается от двух видов ссылок в C++/CLI или даже @nogc в D.

Это разные вещи

Это одно и то же - ручное указание, где использовать GC, а где нет.

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

Ручное управление - это, например, в ядре. Там и в самом деле нужно вручную писать вызов функции, которая уменьшает/увеличивает счетчик ссылок. А с shared_ptr эти функции вызывает компилятор.

Так что ты пока еще не доказал, что shared_ptr — это сборщик мусора.

Я и не собирался это доказывать - только объяснить. Если мое объяснение тебя не убеждает - окей, оставайся при своем мнении.

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

Ахренеть, ты умеешь устраивать утечки памяти. Можно твой автограф?

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

Нет разницы между «компилятор вызывает функции инкремента/декремента» и «компилятор вызывает конструкторы, деструкторы и операторы копирования-перемещения, которые выполняют инкремент/декремент».

Есть. Пусть вызывается деструктор вида:

my_smart_pointer::my_smart_pointer() {
  if(data_) {
    if(0 == dec_ref_count(data_->ref_count_)) {
      delete data_->obj_;
      delete data_;
    }
  }
}
Если я допущу ошибку и забуду декрементировать счетчик, никакого GC у меня не будет. При этом вызовы деструкторов для экземпляров my_smart_pointer компилятор будет вставлять.

Это одно и то же - ручное указание, где использовать GC, а где нет.

Это для языка с GC так можно говорить: здесь GC, здесь не GC.

В случае с C++ (в котором GC нет), этого недостаточно.

Я и не собирался это доказывать - только объяснить.

Ты не объяснил, каким образом shared_ptr относится к GC (пусть даже и на основе подсчета ссылок). То, что shared_ptr использует внутри себя подсчет ссылок еще не переводит его в категорию GC.

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

Плюс tcmalloc это то что его можно использовать неинтрузивно.

Т.е. вот основной юзекейс:

  • Написали многопоточное ПО не парясь о выделении памяти и её освобождении (в смысле используюя всё встроенное в язык/библиотеки).
  • Оптимизировали, что оптимизируется алгоритмически
  • Приделали tcmalloc - получили прирост 25% - задумались, полезли проффилировать аллокации/деаллокации, иначе - даже дёргаться не стоит в большинстве случаев
pon4ik ★★★★★ ()
Ответ на: комментарий от pon4ik

tcmalloc и jemalloc были особенно актуальны, пока, начиная с какой-то версии, в стандартный glibc не впилили новый аллокатор, который стал значительно превосходить предыдущий в многопоточных приложениях. На одном довольно много-жрущем много-поточном приложении после многочисленных замеров я не заметил большой разницы между malloc из glibc, tcmalloc и jemalloc. Может быть, у кого-то по-другому

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

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

Если ты допустишь ошибку при реализации GC - естественно, у тебя не будет GC.

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

Если ты допустишь ошибку при реализации GC - естественно, у тебя не будет GC.

Ну и где тут компилятор, который расставляет инкременты/декременты?

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

А. Я правильно понимаю, что ты доколебался до фразы «компилятор сам расставляет инкременты и декременты»? Имелось в виду «компилятор сам расставляет вызовы функций, реализующих GC, хотя и не знает, что они реализуют GC».

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

иначе - даже дёргаться не стоит в большинстве случаев

Спору нет. Оптимизировать нужно те несколько процентов кода, которые дадут наибольший прирост.

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

Я правильно понимаю, что ты доколебался до фразы «компилятор сам расставляет инкременты и декременты»?

Это и есть ключевое в данном разговоре.

Я тебе еще раз намекаю: GC может использовать подсчет ссылок. Но это не значит, что использование подсчета ссылок приводит к появлению GC.

Так что shared_ptr в плюсах — это не GC.

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

Я тебе еще раз намекаю: GC может использовать подсчет ссылок. Но это не значит, что использование подсчета ссылок приводит к появлению GC.

Я этого и не утверждал (ручной подсчет ссылок в ядре - это не GC).

Так что shared_ptr в плюсах — это не GC.

Я понял твое мнение и не вижу причин соглашаться с ним.

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

Ну и где тут компилятор, который расставляет инкременты/декременты?

А если ты пишешь компилятор языка с ГЦ, но допускаешь ошибку в реализации и в итоге декремента не происходит? Что у нас будет: «отсутствие ГЦ» или «наличие (, но багнутого)»?

Хотя, как по мне, разделять GC и shared_ptr удобно просто с практической точки зрения.

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

А с какой версии?

По факту у меня просто почти везде rhel6, так то, для меня пока не актуально, но это может быть одним из «за» для перехода на «что нить посвежее».

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

Я понял твое мнение и не вижу причин соглашаться с ним.

Какого-либо обоснования своему мнению ты вообще не привел. Так что, если ты придерживаешься мнения, что shared_ptr — это GC, то подтверди это хоть чем-нибудь.

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

Уже не помню. Года полтора прошло, как последний раз работал над тем кодом. В RHEL6 вроде как должно быть уже. Вот, в RHEL5 может быть и нет

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

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

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

Так что, если ты придерживаешься мнения, что shared_ptr — это GC, то подтверди это хоть чем-нибудь.

https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

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

https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)#Reference...

«Reference counting is a form of garbage collection whereby each object has a count of the number of references to it. Garbage is identified by having a reference count of zero. An object's reference count is incremented when a reference to it is created, and decremented when a reference is destroyed. When the count reaches zero, the object's memory is reclaimed.»

Какое из этих утверждений не выполняется для shared_ptr?

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

т.к. компилятор расставляет только вызовы конструкторов/деструкторов, а правильное наполнение их лежит на совести программиста.

А если нужное наполнение имеется «из коробки»?

Я к тому, что не очень понимаю, где проходит грань - всё-таки компилятор тоже пишут программисты и на их совести многое находится. Опять же, (какой-нибудь) язык может не иметь GC из коробки, но иметь все средства для нормальной реализации такового в виде библиотеки. Будет ли это GC, учитывая, что в библиотеке могут быть баги?

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