Мысли о эффективном автоматическом управлении памятью
От: VladD2 Российская Империя www.nemerle.org
Дата: 27.10.14 19:16
Оценка: 93 (11) +1 -5
Я давно задумывался над этой темой, но последние баталии на эту тему подтолкнули меня, чтобы сформулировать свои мысли по этому поводу.

Предпосылки


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

Понятно, что обязательным слагаемым быстрой и безопасной разработки является автоматическое управление памятью. На сегодня в этой области есть только:
1. Автоматический подсчет ссылок — ARC.
2. Сборка мусора — GC.

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

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

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

Теперь немного "крамолы" (для не посвященных). На самом деле скорость GC существенно выше чем скорость ARC или ручного управления памятью на базе malloc/free, так как выделение памяти требует всего лишь одного приращения указателя, а освобождение не стоит ничего (мортвые объекты просто игнорируются). Платим мы только за поиск живых объектов и за дефрагментацию кучи. Поиск живых объектов дешевле чем подсчет ссылок (точнее приблизительно равен, но подсчетом ссылок нужно заниматься чаще). А дефрагментации в системах отличных от GC попросту нет. Так что тут и сравнивать не чего. Можно же было просто возвращать объекты в список свободных и выделять повторно. Просто дефрагментация избавляет от ряда проблем. Особенно от фрагментации кучи (ваш КО).

Так почему же языки вроде С и С++ зачастую быстрее управляемых (т.е. использующих GC)?

Ответ не так прост:
1. Примитивные техники GC дают довольно заметные остановки всего приложения для подсчета живых ссылок и дефрагментации кучи. Чтобы этого избежать применяются разные продвинутые техники вроде бэкграунд-пометки живых объктов и даже икрементальной дефрагментации. А чтобы это было возможно код связанный с присвоением указателей заменяют специальными перехватчиками которые отлавливают изменение уже помеченных объектов и т.п. Вот этот то код и создает основной оверхэд от GC, а вовсе не сам GC. В серверных GC этого кода может и не быть, но они могут давать заметные даже не глаз задержки. Причем в некоторых приложениях (где графы объектов малы) задержек не видно. А в некоторых они могут быть вдины.
2. Качество оптимизаций компилятора (джита или нгена для дотнета). Это отставание обусловлено двумя факторами. Во-первых, очевидным лидерством плюсовых компиляторов (в них вложено уже очень много денег и сил). Во-вторых, некоторыми особенностями управляемых языков которые препятствуют агресивным оптимизациям. Эту тему я затрагивать не буду, так как мой спичь о GC. Но учитывать это надо. Так как во многом отставание GC-языков определяется вовсе не наличием GC.
4. В С/С++ можно использовать ряд хаков для более эффективного управления памятью выделенной в куче. Но они лишь чуть-чуть могут опередить GC, так как в нем и так применяются найэфективнейший алгоритмы.
5. И, наконец, главное! Тем что в С и С++ большая часть данных размещается вовсе в куче, а на стеке!
6. Гранулярностью куч и необходимостью защиты их от многопоточной среды. Многие GC-среды имеют одну или две кучи (не считая поколений). Так в дотнете по умолчанию используется две кучи SOH и LOH. Одна — SOH — используется для объектов до 85 000 байт (в ней есть поколения и производится дефрагментация). Другая — LOH — используется для размещения больших объектов. В ней не производится дефрагментации (хотя в 4.5 вроде бы начали что-то делать) и все объекты в ней сразу же причисляются к второму (последнему) поколению.

Так вот вот эффективность управления памятью и размеры пауз связанных с GC зависят от двух факторов:
1. Малому размещению объектов на стеке. Это связано с тем, что языки спроектированные в расчете на GC, на сегодня, размещают объекты имеющие виртуальные таблицы и на которые можно дать ссылки, только в куче.
2. Сваливанию всех объектов в одну кучу. В больших приложениях графы объектов достигают гигантских размеров, так что сборка последних (не эфемерных) поколений может вызывать заметную на глаз задержку.

Альтернативы GC


На сегодня я знаком с двумя безопасными альтернативы GC:
1. ARC (автоматический подсчет ссылок).
2. Ручное управление временем жизни объектов с контролем со стороны компилятора.

На практике второй пункт без ARC или GC попросту не живет. Да и представитель у такого подхода по сути ровно один — Rust.

В двух словах идея управления жизни в Rust сводится к тому, что объекты можно размещать на стеке или в куче (как в С/С++). Но при этом есть специальный вид размещения в куче:
Box — которая обеспечивает условие одиночного владения объектом в единицу времени (т.е. это уникальная ссылка, но контролируемая компилятором).
ARC — ну, с ней все ясно.

Собственно интерес в Rust представляет, в первую очередь, идея явного управления временем жизни объектов и ручной связи с ними времени жизни указателей. Идея простая как пробка — указатель должен жить не больше чем объект на/в который он указывает. Так чтобы возвратить указатель указывающий внутрь некоторого объекта переданного в функцию, возвращаемый указатель должен быть ассоциирован с временем жизни параметров на который он может указывать. Тогда компилятор сам отследит чтобы пользователь функции не использовал указатель когда кончится время жизни объекта.

Идея интересная, но уж слишком много, при этом, ложится на плечи программиста.

Причем у этого подхода есть и ограничения. Так указатель в Rust не отслеживается, а значит он не должен сдвигаться. Это значит, что точный GC (GC поддерживающий дефрагментацию) вряд ли может быть использован в Rust.

Еще одна проблема — автоматическое управление объектами в куче на которые нужно иметь более одной ссылки делается через ARC.

Собственно, а чем плох ARC? ARC, ведь, можно рассматривать как вид GC.

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

Можно ли улучшить языки рассчитанные на GC


Я думаю, что это вполне возможно. Каковы средства оптимизации программ в управляемом мире? Да очень просты! Программисты стараются по меньше занимать памяти в куче!

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

Если каким-то образом позволить (в GC-языках) размещать большую часть объектов на стеке, то производительность существенно увеличится.

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

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

Предложения по сокращению оверхэда от GC


1. Нужно сделать очень простую модификацию в языках (и рантайме) — ввести ссылку которую нельзя а) помещать в поля обычных объектов или массивы; б) которую нельзя возвращать за пределы области видимости где она создана. Это позволит описывать большинство функций как принимающие такие указатели, т.е. гарантированно их не прикапывающие ссылки или прикапывающие в объектах чье время жизни меньше или равно объекту на который делается ссылка.
2. Ввести уникальные указатели, позволяющие владеть объектом только одному объекту (это позволит так же упростить мнонопоточное программирование).
3. Увеличить количество куч в управляемых программах. Тут возможны варианты, но мне больше нравится идея акторов:
* Вводим по одной кучи на поток. Запрещаем обращение к памяти из этой кучи из других потоков.
* Вводим специальную обменную кучу в которой разрешаем размещать исключительно неизменяемые объекты.
* Вводим для каждого потока очередь в которой можно помещать объекты из обменной кучи.
Так же ввести глобальную кучу рэйдонли-объектов куда можно помещать совместно используемые объекты и константы.
Еще можно позволить вводить специальные локальные кучи с фиксированным временем жизни (по стеку или с ручным уничтожением). После уничтожения кучи можно просто пометить все ссылки на нее нулями или даже выкидывать исключение, если таковые ссылки имеются. В прочем, и без этого варианта предлагаемые изменения должны снять проблемы которые обычно ассоциируют с GC.

Заключение


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

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