LINUX.ORG.RU

Вопрос по ручному управлению памятью

 ,


1

2

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

Меня заинтересовал вот какой вопрос. Допустим, мы пишем команду «освободить память такую-то». Интересно, что реально тут происходит в этот момент? Исполнение останавливается до тех пор, пока память не будет освобождена, а затем возобновляется? В таком случае, у нас разница между языком с GC и без него только в том, что нет накладных расходов на сам GC, на алгоритмы подсчета ссылок и/или обход дерева. Непосредственно на расход памяти это не влияет, практически, так получается?

И, кстати, что представляет из себя процесс освобождения памяти, на более низком уровне?

ЗЫ пришлось поставить тег c++, поскольку тегов связанных с «просто си» не нашел вообще. Подскажите что поставить по си.



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

сколько усилий нужно приложить к тому, чтобы создать сложную динамическую структуру данных (скажем, граф с циклическими ссылками между узлами)

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

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

Всякие unique_ptr - это всё ещё «ручное» управление, если противопоставляем мы ГЦ.

Это полуавтоматическое. Т.е. управлять руками уже не надо, а вот думать еще приходится=)

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

GC в большинстве случаев не избавляет от необходимости в weakptr.

У weak_ptr для RC и weak_ref в различных GC несколько разные задачи. В системе с GC циклы не являются проблемой в принципе. А именно это является основной причиной использовать weak_ptr в языках без GC. weak_ref же нужен в гораздо более редких случаях, когда тебе нужно иметь ссылку, которая живет настолько долго, что мешает нормальному освобождению памяти, но при этом не настолько важна, чтобы идти на это.

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

Что значит расставляет?

Это значит, что никто кроме программиста не преобразует вот такой код:

auto p = new some_class{...};
в код с использованием RAII, вроде вот такого:
auto p = make_unique<some_class>(...);
И даже если программист использует RAII, это не избавляет его от внимания к некоторым тонкостям, вроде вот таких:
auto p = make_unique<some_class>(...);
transfer_object_somewhere(move(p));
...
p->call_some_method();

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

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

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

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

Если «ручное» определяется через «явно», может стоит использовать слово «явно» явно?

Если разработчик создаёт объект в функции с множеством выходов, компилятор вставляет деструкторы за него. Вместо ручной рутиной операции — автоматизированная.

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

Над работой с памятью и зависимостью между объектами. Я не против, если что=)

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

forCe
()

Меня заинтересовал вот какой вопрос. Допустим, мы пишем команду «освободить память такую-то». Интересно, что реально тут происходит в этот момент? Исполнение останавливается до тех пор, пока память не будет освобождена, а затем возобновляется?

Да.

В таком случае, у нас разница между языком с GC и без него только в том, что нет накладных расходов на сам GC, на алгоритмы подсчета ссылок и/или обход дерева. Непосредственно на расход памяти это не влияет, практически, так получается?

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

Есть GC, основанные на технологии подсчёта ссылок. В них дополнительного расхода памяти нет, но они менее эффективны и не могут справляться с циклическими указателями.

И, кстати, что представляет из себя процесс освобождения памяти, на более низком уровне?

Участок памяти помечается, как свободный во внутренних структурах менеджера памяти. Возможно он будет возвращён операционной системе.

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

Это значит, что никто кроме программиста не преобразует

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

это не избавляет его от внимания к некоторым тонкостям

GC от этого так же не избавляет. Думаю, пример с boehm gc сам сможешь написать=)

Это другие особенности языка сказываются(в Rust, например, такого не будет). Не надо валить все в одну кучу.

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

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

Вы же сами описали разницу между weak_ptr для схемы с RC и для случая с GC.

Под убогим GC я понимал реализации GC на основе RC, которые ранее встречались в реализациях скриптовых языков (емнип, Python как раз был из таких в свое время) и в которых weak_ptr использовался как раз потому, что столь примитивный GC не справлялся с циклическими графами.

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

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

Но ведь в случае RC это не так=) Когда будет освобождена память заранее сказать будет нельзя(иначе бы всегда и везде работали механизмы «статического подсчета ссылок» или хотя бы вывода регионов).

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

Неужели ты из тех, кто явно зануляет указатели после освобождения?

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

Но иногда таки они возникают. Когда ты обнаруживаешь, что твое приложение под JVM жрет как ни в себя и ему мало 120гигов на сервере, приходится что-то делать. Самое интересное, что я в своей практике особой разницы не вижу. Под JVM даже сложнее все это понять, отладить и, главное, исправить. Проще докупить серваков=)

А задачи следить за корректностью ссылок, которые могут указывать на удаленные объекты, нет в принципе.

Это заслуга не GC, а языка. Rust справляется с этими проблемами без GC.

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

Не надо валить все в одну кучу.

Речь не о всей куче, а о том, что якобы «в C++ почти не осталось ручного управления памятью». Если включение в стандарт unique_ptr на пару с shared_ptr означает исчезновение ручного управления памятью в C++... Ну значит мир вокруг стал совсем другим, а я и не заметил.

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

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

Нет, я с этим тезисом не согласен и оспорил его, как вы заметили. Но weak_ref'ы нужны в случае GC. Другое дело, что гораздо реже, чем weak_ptr в RC.

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

ему не надо об этом думать

это смотря с какой стороны посмотреть. Допустим

newString = string.dosmth
//vs
string = string.dosmth
ты же думаешь, что в первом случае у тебя будут накладные расходы с хранением 2 строк вместо одной. Если тебе оригинал не нужен, ты выберешь 2-ой вариант. Поэтому, нельзя сказать, что ты уж совсем не контролируешь расход памяти. Просто, вместо того, чтобы вручную удалять что-то, как бы кидаешь это GC, чтобы он это сделал.

newquestion
() автор топика
Ответ на: комментарий от forCe

Когда будет освобождена память заранее сказать будет нельзя(иначе бы всегда и везде работали механизмы «статического подсчета ссылок» или хотя бы вывода регионов).

Это вы сейчас ерунду написали. Разрушение объектов, находящихся под счетчиком ссылок осуществляется при занулении этого счетчика. А зануление может произойти в одном из четко определенных мест в приложении — разрушения очередной обертки вокруг счетчика. Соответственно, задача программиста — обеспечить разрушение этих оберток.

Неужели ты из тех, кто явно зануляет указатели после освобождения?

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

Rust справляется с этими проблемами без GC.

И поэтому там целая куча разных типов счетчиков ссылок на объекты. А так же контроль со стороны компилятора за действиями пользователя. Т.е. тот самый геморрой, которого не, если в языке нормальный GC.

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

Другое дело, что гораздо реже, чем weak_ptr в RC.

О том и речь.

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

Такое бывает нужно довольно редко и не является обычной практикой. Если ты переменную больше не используешь, компилятор и так поймёт, что она больше не нужна и удалит её.

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

Такое бывает нужно довольно редко и не является обычной практикой. Если ты переменную больше не используешь, компилятор и так поймёт, что она больше не нужна и удалит её.

А причем тут неиспользуемая переменная? Речь идет об объекте, на который указывает переменная. Если ты выставляешь переменную в null, или удаляешь, на объект, который она указывала, перестают существовать ссылки, и он удаляется GC. А оттого что ты переменую в null установил, она никуда не денется, она и дальше будт указывать на null.

И я бы не сказал, что это редко используется.

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

А причем тут неиспользуемая переменная? Речь идет об объекте, на который указывает переменная. Если ты выставляешь переменную в null, или удаляешь, на объект, который она указывала, перестают существовать ссылки, и он удаляется GC. А оттого что ты переменую в null установил, она никуда не денется, она и дальше будт указывать на null.

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

И я бы не сказал, что это редко используется.

В Java очень редко. Я даже не припомню, когда я в последний раз это использовал.

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

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

ты как-то извращенно-усложненно все трактуешь. Просто эта переменная не указывает больше на объект, который ранее указывала, а указывает на нул. Соответственно, тот объект на который она ссылалась, будет удален при следующем проходе GC. Сама переменная никуда не делась, она тут вообще не причем. В js есть еще оператор delete, который удаляет переменную, но с точки зрения памяти, это то же самое, практически.

newquestion
() автор топика
Ответ на: комментарий от Legioner

неиспользуемой переменной

она не является «неиспользуемой», это обычная переменная, которая ссылается на объект, только объектом в данном случае, является null.

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

Кстати, в C++ почти не осталось ручного управления памятью.

Категорически не согласен. shared_ptr, weak_ptr, uniq_ptr — вполне себе «ручное управление памятью».

kawaii_neko ★★★★
()

очень интересный тред, я прямо зачитался

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

Тогда твоё мнение очень странное.

Подсчет ссылок или ручное освобождение вызовом free — разница невелика. После talloc, все эти *_ptr выглядят как адский никому не нужный тяжелый ручной труд.

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

shared_ptr, weak_ptr, uniq_ptr — вполне себе «ручное управление памятью».

Ручное имеет смысл только, когда оно «осмысленное». Ручное не осмысленное - лишь убогость интерфейса.

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

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

Разделение автопоинтеров на 3вида - это убогость крестовой реализации

А разделение арифметических действий на четыре вида — убогость арифметики?

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

Разделение автопоинтеров как минимум на два вида (shared и weak) продиктовано самой убогостью концепции подсчета ссылок, в которую не ложатся циклически связанные структуры.

Спору нет, shared-владение само по себе бывает полезно, скажем, когда мы раздаем один и тот же видеопоток на кучу клиентов — вместо дублирования фрагментов, создаем кучу указателей на него и грохаем фрагмент, когда он был отдан последнему пользователю. Но когда на refcounting'е пытаются сделать ВСЕ, получается как в перле, питоне, js: забыл занулись ссылочку в словаре — здравствуй, утечка памяти.

GC в этом плане намного более правильная концепция, но уж больно дорогая по всем параметрам.

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

После talloc, все эти *_ptr выглядят как адский никому не нужный тяжелый ручной труд.

Экхм. А на C++ ты пишешь? А то у тебя взаимоисключающие суждения.

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

Но ведь если ты не «занулишь ссылочку в словаре» и словарь будет жить, то и с GC ты получишь утечку

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

ВСЕ, получается как в перле, питоне, js: забыл занулись ссылочку в словаре — здравствуй, утечка памяти.

ты никогда не забудешь это сделать, если ты пишешь код осознано.

newquestion
() автор топика
Ответ на: комментарий от anonymous

Если словарик со strong-ссылками, то да. А там может быть и weak map, которому занулять последнюю ссылку из словаря не надо. В Java вон вообще есть и soft-ссылки, которые GC сам может взять и почистить, если ему памяти мало.

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

Это вы сейчас ерунду написали. Разрушение объектов, находящихся под счетчиком ссылок осуществляется при занулении этого счетчика.

Помню лет 10 назад у меня был проект, в котором у нас отдельный тред удалял объекты с нулевым счетчиком=) И это таки RC, а не GC.

А зануление может произойти в одном из четко определенных мест в приложении — разрушения очередной обертки вокруг счетчика.

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

И поэтому там целая куча разных типов счетчиков ссылок на объекты. А так же контроль со стороны компилятора за действиями пользователя. Т.е. тот самый геморрой, которого не, если в языке нормальный GC.

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

forCe
()

Отличия от управляемых языков тут два:

1) Память в Си освобождается небольшими блоками по мере необходимости, как правило. GC же спит, а потом в один прекрасный момент начинает освобождать сразу много блоков памяти, которые успели накопиться пока он спал. В итоге то что размазано по всему времени работы программы в Си, собирается в один момент времени на Java и получаются тормоза. Поясню: если программа работает на 5% медленнее, ты это никогда не заметишь. А если она работает нормально, но раз в несколько секунд работает в 10 раз медленнее, это будет сильно раздражать.

2) В Си память освобождается как только перестаёт быть нужной. В Java - только когда сработает GC. В итоге при интенсивной работе с ней, в Java много времени будет висеть память, которая ожидает освобождения, а новые блоки будут выделяться из кучи, а не из этой памяти. В итоге объём потребления памяти возрастает, потому что ОС не важно нужные ли данные в ОЗУ - она всё равно занята, пока приложение не сообщит об обратном. Аналогия: если мыть посуду сразу после еды, то количество грязной посуды будет минимально (по сути дела это будет лишь та посуда, из которой едят прямо сейчас). А если мыть посуду массово, но только каждую пятницу, то количество грязной посуды будет сильно больше (при одинаковом потреблении каждый день - в 7 раз). Более того - не все смогут себе такое позволить, потому что чистая посуда может и кончится в середине недели.

А также особенность, которая не связанна прямо с управлением памятью - в Java насаждается концепция «всё есть объект» (даже числа) и в JVM помимо основной программы работает куча других вещей - JIT, например. В итоге накладные расходы по памяти растут.

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

Помню лет 10 назад у меня был проект, в котором у нас отдельный тред удалял объекты с нулевым счетчиком=) И это таки RC, а не GC.

Я рад за вас, но это ничуть не опровергает то, что я говорил про ручное управление памятью. Скорее только подтверждает.

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

Объекты от этого жить не перестают.

Во-первых... Во-вторых... В-третьих...

Вместо этого хотелось бы узнать, с каких пор в C++ исчезло ручное управление памятью. Или вы не разделяете точку зрения i-rinat?

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

shared_ptr, weak_ptr, uniq_ptr — вполне себе «ручное управление памятью».

А аргументировать вы не хотите?

{
    auto a = make_unique<A>(10, "aaa", 20.2);
    a->foo(10);
    a->bar("asaaa");
}

Где я тут памятью вручную управлял?

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

Объекты от этого жить не перестают.

Они живут до ближайшего прохода GC, можно считать что перестают.

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

Я рад за вас, но это ничуть не опровергает то, что я говорил про ручное управление памятью. Скорее только подтверждает.

Давайте не будем столь стремительно менять контекст. Речь была о RC и заранее продуманном на стадии проектирования времени удаления объектов.

Объекты от этого жить не перестают.

Что значит «жить»? Ведь кода, который бы их использовал не существует=) Опять же, никто не мешает вам в случае RC удалять объекты отложено.

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

А я и не говорил, что оно исчезло. Но если раньше оно было практически в каждом проекте, то сейчас есть отчетливая тенденция отказа от него в пользу т.н. полуавтоматического(уникального владения и подсчета ссылок).

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

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

Давайте не будем столь стремительно менять контекст.

Это не я меняю контекст. Было утверждение о том, что в C++ почти нет ручного управления памятью. Я был этому сильно удивлен, захотел уточнить и объяснил, почему варианты с RAII для меня по-прежнему являются ручным управлением памятью, а не GC.

Что значит «жить»? Ведь кода, который бы их использовал не существует=)

Хотя бы то, что если объект держит ссылку на внешний ресурс и свобождает этот ресурс в финалайзере, то от обнуления ссылки на объект финалайзер автоматически не вызовется.

А вот если речь идет о unique_ptr/shared_ptr, то произойдет (деструктор будет вызван сразу же).

Опять же, никто не мешает вам в случае RC удалять объекты отложено.

В этом случае речь будет идти о чем-то, что в стандарт C++ пока не входит. И к RAII, который в данном контексте был упомянут, так же будет иметь косвенное отношение.

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

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

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

eao197 ★★★★★
()
Ответ на: комментарий от forCe
{
    auto a = make_unique<A>(10, "aaa", 20.2); // <-- HERE
    a->foo(10);
    a->bar("asaaa"); 
} // <-- HERE

Вы вручную не вызывали new/delete. Но памятью управляли.

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

Там, где комментарии стоят.

В первый раз динамически создавая объект и размещая указатель на него в std::unique_ptr. Тем самым определяя время жизни объекта, а именно моментом разрушения этого самого unique_ptr.

Во второй раз в месте закрывающей фигурной скобки, тем самым точно отмеряя, где именно время жизни unique_ptr завершается.

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

объяснил, почему варианты с RAII для меня по-прежнему являются ручным управлением памятью, а не GC.

Ну а я объяснил, почему я с этим не согласен.

Хотя бы то, что если объект держит ссылку на внешний ресурс и свобождает этот ресурс в финалайзере, то от обнуления ссылки на объект финалайзер автоматически не вызовется.

Финалайзеры есть далеко не во всех сборщиках. А там где они есть, в принципе, дискуссионный вопрос пользоваться ли ими. Ибо никто их вызов, строго говоря, не гарантирует. И ресурсами обычно управляют все тем же RAII, только явным(try-with-resources/using/with) или костыльно-велосипедно-копипастным кодом в finally.

А вот если речь идет о unique_ptr/shared_ptr, то произойдет (деструктор будет вызван сразу же).

И это, как правило, хорошо. Но я не стал бы сравнивать финалайзеры с деструкторами - это разные механизмы. И да, вызов деструкторов не имеет отношения к управлению памятью. Скажем, деструктор можно вызывать сразу, а память освобождать отдельно.

И к RAII, который в данном контексте был упомянут, так же будет иметь косвенное отношение.

А вот тут вы меня поймали. Вы правы, если разговор о RAII применительно к памяти, то этот аргумент не работает. Но ведь мы не только о RAII говорим.

Тем не менее, это всегда считалось ручным управлением памятью.

Кем считалось?=) Моя память говорит мне, что при внедрении полуавтоматических механизмов, в разговорах на совещаниях и в курилках, мы всегда противопоставляли умные указатели ручной работе с памятью. Да, это не GC.

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

Разрушение объектов, находящихся под счетчиком ссылок осуществляется при занулении этого счетчика.

После зануления счетчика.

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

Вы правы, если разговор о RAII применительно к памяти, то этот аргумент не работает. Но ведь мы не только о RAII говорим.

Ok, мне кажется, я понял вашу точку зрения. Свою я так же уже несколько раз пояснял: в современном C++ ручное управление памятью никуда не исчезло, оно по-прежнему здесь, поскольку если сравнивать с языками с GC, управлению памятью в C++ нужно уделять значительно больше внимания.

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

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