Использование памяти
Все жалуются на свою память, но никто не жалуется на свой ум.
Ф.Ларошфуко
Модным поветрием последних времён стало активное использование «кучи»,
т.е. динамической памяти, для размещения там объектов.
Даже если данные локальны и используются только внутри функции,
всё равно данные зачем-то размещают в «куче»,
в стеке остаётся только указатель на объект.
Про компоненты VCL в книгах про C++ Builder написано: конструктор работает
только с оператором «new».
Все экземпляры классов языка Java, или объекты, создаются с «new».
Объекты кроссплатформенной библиотеки Qt тоже создаются только с помощью «new».
Такое впечатление, что операция «new» теперь внесена в набор команд процессоров x86,
а каждая микросхема памяти имеет заначку сверх заявленной ёмкости специально для «кучи».
Вот пример на C++ Builder:
void func() {
TOpenDialog* dlg = new TOpenDoalog(this);
dlg -> Title = "Open a New Dialog";
dlg -> Execute();
}
Если бы упомянутая выше функция возвращала бы указатель на объект, то этому можно найти оправдание:
TOpenDialog* func(this) {
return new TOpenDoalog(this);
}
TOpenDialog* dlg -> func(this)
dlg -> Title = "Open a New Dialog";
dlg -> Execute();
TForm* ptr = new TForm();
// ...
Но зачастую локальные данные функции создаются в «куче», используются, а затем по окончании удаляются.
Чтобы не забыть удалить уже ненужные объекты, используются технологии вроде «умных указателей».
Только лишь потому, что C/C++ нет гибких средств работы со стеком.
В PHP же, к примеру, программист вообще не распоряжается памятью.
За него это делает интерпретатор, и делает он с помощью той же «кучи».
Язык Java пользуют оператор «new» и в хвост, и в гриву:
SomeObject value = new SomeObject();
Если Вы программируете на Форте, то там всё ровно наоборот.
Форт даёт большую гибкость в работе со стеком.
Но Вы наверняка Фортом не пользуетесь, поэтому не знакомы с такой гибкостью и ничего не знаете о других, присущих только Форту тараканах.
Конечно, закон Мура давно служит верой и правдой тем, кто не задумывается над эффективными решениями.
Современные вычислительные мощности таковы, что даже бесконечный цикл, по мнению некоторых, выполняется за 6 секунд.
Да, алгоритмы распеределения динамической памяти как никогда эффективны (или память «дёшева»?).
Но всё равно нет доказательств того, что «куча» быстрее стека,
что локальные объекты лучше размещать в динамичекой памяти.
А вот интересный комментарий к статье:
понадобилось под Win 2003 Server x64 быстро
раздавать по HTTP статические файлы — все .Net было решено ликвидировать...
когда оказалось, что IIS 6 кушает около сотни мегабайт при раздаче статических файлов...
создаем, например, System.List<> и работаем с коллекцией.
Хорошо, удобно, красиво.
Теперь представим, что такая функция вызывается очень и очень часто.
В моем случае это было извлечение пользоватльских пакетов из очереди, простое шифрование и передача в сокет.
Каждый экземпляр System.List<> это класс, реализующий пяток интерфейсов и имеющий два десятка виртуальных методов.
Внутри него находится типизированный System.Array с нашими данными. Это ещё один класс со схожими интерфейсами и наследованием.
Оба создаются в хипе, а не в стеке, что бы не рассказывали нам об умном JIT.
...
При высокой нагрузке приложения отслеживается заметный рост загрузки процессора потоками GC.
Замена List массивом (т.е. избавление от одного из сложных классов) заметно уменьшает нагрузку на GC.
...
Вывод прост: System.Array давно перестал быть массивом...
больше нет возможности создаваться в стеке.
Да, есть некоторые неудобства работы со стеком.
Компиляторам знать вовремя компиляции размеры всех объектов и их количество.
Тогда они в состоянии жёстко прописать для каждого объекта смещения
относительно вершины стека.
Если объект имеет длину, неизвестную на момент компиляции, то как ему
отвести место в стеке?
Например, функции передаётся параметр, характеризующий длину локальной строки:
void function (int N, int M) {
char Str1 [N];
char Str2 [M];
// ...
}
Естественно, эта длина будет известна только к началу выполнения функции.
Решение, позволяющее размещать в стеке объекты переменного размера, появилось
в C/C++ относительно недавно, в стандарте
ISO C99.
Многие компиляторы его до сих пор не поддерживают.
А если всё происходит в динамике?
void function () {
// ...
while (condition)
{ int N = foo ();
char Str1 [N];
}
// ...
}
А если всё происходит в динамике? Без «кучи» не обойтись?
Возможно, вместо аппаратного стека можно попробовать использовать программный стек.
Он будет проигрывать в эффективности аппаратному,
но всё равно окажется быстрее динамической памяти.
Реализация:
Стек вызовов может быть реализован как в виде специализированного стекового регистра ограниченной глубины
(или даже обычного регистра адреса возврата, например в некоторых моделях PowerPC),
так и в виде указателя вершины стека в оперативную память или регистровый файл процессора.
При отсутствии или ограниченности стека, вложенные вызовы исключены или их количество ограничено.
При необходимости бо́льшей вложенности, стек вызовов или его расширение могут быть реализовано программно.
Но, если разобраться поглубже, то и программный стек не особо нужен.
Система команд x86 достаточно развита, и в дальнейшем мы разберёмся, как с достичь желаемого.
Читаем далее следующую статью:
Почему динамическое распределение памяти — это плохо.
Что ещё почитать на эту тему
Опубликовано: 2014.07.15, последняя правка: 2019.01.28 20:37
Отзывы
✅ 2016/04/03 07:54, rst256 #0
Да можно в Си и через стек передавать все что угодно, только это обычно просто неприменимо исходя из условий задачи. Например, после получения блока данных их нужно засунуть во внешнюю функцию, вы знаете, когда можно будет освободить данный блок? Что в итоге будет если передавать через стек, сначала гоним блок по стеку, а потом или все равно переносим его кучу. Или не переносим, тогда будем наблюдать интереснейшие баги и возможно sigsegm.
Принцип стека бесспорно входит в 10-ку основных методов работы с памятью, но не будет идеальным и одновременно универсальным решением для реальных задач. Если нужен доп. стек, просто выделяют из кучи блок желаемого размера, и в нем же хранят все его параметры. Аппаратный стек не трогают! И явно стековые операции со входами/выходами их функций не связывают, в этом просто нет смысла. Пример. Вызываем некую ф-ю ф1, которая заносит нечто в наш программный стек. Выходим, вызываем ф2, в ней происходит вызов ф22, ф23 и т.д. ... -/-/- тоже ф3, ф33, ... и так много раз, а данные помещенные ф1 в стек предназначены для ф001 которой тут нет, она на 4 уровня выше, так что мы должны тащить данный стек сквозь все вызовы внутри этой ф-и и ещё потом на 4 уровня вверх? Для правильного ответа нужно учитывать тот факт, что определить, где и насколько долго будут нужны возвращаемые функцией данные, есть очень нетривиальная задача. Ах да чуть не забыл, есть вероятность, что данный стек используется несколькими потоками, нужны блокировки, семафоры и проч. А то как же без реентарности то, без реентарности сейчас никуда.✅ 2016/04/03 17:33, Автор сайта #1
Принцип стека бесспорно входит в 10-ку основных методов работы с памятью Вообще-то основных способов три: статическая память, динамическая («куча») и стек. Всё остальное — от лукавого. Или вы можете назвать что-то ещё (места с четвёртого по десятое)?после получения блока данных их нужно засунуть во внешнюю функцию, вы знаете, когда можно будет освободить данный блок? Записываем данные во внешнюю функцию, после возврата ей управления данными распоряжается именно эта функция. определить, где и насколько долго будут нужны возвращаемые функцией данные, есть очень нетривиальная задача. Вполне тривиально для однопоточного программирования. Вызывающая функция получает данные, она ими распоряжается до тех пор, пока не умрёт. Или не передаст права на управление, но это уже затрагивается концепция прав владения.Ах да чуть не забыл, есть вероятность, что данный стек используется несколькими потоками, нужны блокировки, семафоры и проч. А то как же без реентарности то, без реентарности сейчас никуда. Выделение памяти в динамике в стеке не стоит использовать в следующих случаях:- При многопоточном программировании, когда функция вместе со своим стеком умирает раньше, чем поток, который использует данные.
- Функция должна получать, хранить и освобождать данные, при этом порядок освобождения памяти произволен, т.е. нарушен принцип LIFO.
Да, с многопоточным программированием сложнее, тут и реентерабельность нужна, и много ещё чего.✅ 2016/04/11 18:18, rst256 #2
Вообще-то основных способов три: статическая память, динамическая («куча») и стек. Всё остальное — от лукавого. Ну раз такое дело, есть только память от 0 и до ..., Все остальное и правда от лукавого. Я могу статически выделить память под стек? А из кучи? Так может статическ./динамическая память это лишь способ выделить блок памяти, а стек или скажем очередь(вот вам и 4-ое) описывают уже как оперировать данным блоком?Вполне тривиально для однопоточного программирования. Очень нетривиально уже даже там:f1(...){ char *s = f2(...); some_func(s); } some_func — из dll, вы уверенны что к s не будет обращений после выхода из f1?static char *S0 = NULL; some_func(char s*){ S0=s; } char * some_func2(){ return S0; } Вызовем же some_func2 после выхода из f1, и подсунем возврат printf, возможно даже будет работать :-) Как видим с однопоточным тоже довольно непросто.
А ещё интересно как на такие игры с esp отреагируют эвристика антивирусной программы? Может попробовать отправить образец такого кода (скомпилированный конечно) на virustotal.com, хорошо бы ещё если он будет производить системные вызовы (ну к user32 ещё к чему не важно)
P.S. Кстати, любой код, использующий GUI, будет многопоточным.void func() { TOpenDialog* dlg = new TOpenDoalog(this); dlg -> Title = "Open a New Dialog"; dlg -> Execute(); } dlg будет жить гораздо дольше, чем вызов func, nicht wahr?✅ 2016/04/13 16:01, Автор сайта #3
Я могу статически выделить память под стек? А из кучи? Компилятор выделяет программе три названных выше блока памяти. А дальше программа может взять кусок памяти от любого из трёх блоков памяти и создать какой-то свой контейнер: стек, дек, очередь, массив, ассоциативный массив, дерево и т.д. В статической области памяти память под этот контейнер надо зарезервировать статически, в остальных случаях она выделяется в динамике.
В приведённом примере нетривиальной является передача указателя на память, которая вскоре исчезнет. Т.е. это запланированная ошибка. Для исключения подобных ситуаций существует концепция прав владения, её надо выработать и для нового языка. Так что при однопоточности, когда нет утечек наружу указателей на локальную память, всё остаётся в силе: в стеке можно резервировать память, если соблюдается дисциплина LIFO.как на такие игры с esp отреагируют эвристика антивирусной программы? Интересные вопросы Вы задаёте. Я даже не задумывался об этом. Антивирус на моём компьютере молчал всё это время. любой код, использующий GUI, будет многопоточным. Не только красота, но и простота спасёт мир. В DOS не было многопоточности, но были прерывания. Прерывания — это очень просто: либо работает обычный код, либо код, обрабатывающий прерывание, но вместе — никогда. Поэтому там не нужны были блокировки, мьютексы и т.д.
WinAPI устроено весьма похоже: есть поток сообщений, обработка которых похожа на обработку прерываний. GUI WinAPI эксплуатирует идею «рисования по свистку». Т.е. произошло событие — вот тогда и рисуем. Для рисования в Windows опять же не нужны блокировки ресурсов и прочее. Не могу сказать за всю Одессу все разновидности GUI, но «мастдае» как-то так.dlg будет жить гораздо дольше, чем вызов func Судя по коду, который вы привели, указатель на память dlg умрёт вместе с функций. Ведь функция имеет тип void (dlg не возвращается), переданный ей список параметров пуст (dlg не сохранятся через переданный указатель), код не пишет в статическую память (dlg не сохраняется вовне, в глобальной переменной). Следовательно, dlg умрёт вместе с родительской функций. А вот память, на которую ссылается dlg, останется жить. Имеет место быть классическая «утечка памяти»: выделенная память не уничтожается вместе с указателем. Это тот случай, который хороший язык программирования должен предотвратить: надо либо передать dlg куда-то вовне, либо вызвать delete. Опять возвращаемся к концепции владения.
Кстати, если оператор new переопределён таким образом, что память выделяется не в «куче», а в стеке, то при выходе из функции уничтожается как выделенная в стеке память, так и указатель на неё.✅ 2016/08/06 19:54, rst256 #4
Интересные вопросы Вы задаёте. Я даже не задумывался об этом. Антивирус на моём компьютере молчал всё это время. На virustotal.com попробуйте закинуть скомпилированный файл, там сразу по всем антивирусам прогонят его.В DOS не было многопоточности, но были прерывания. Ну они и сейчас есть, и DOS тут не причем, это же часть архитектуры процессора.Прерывания — это очень просто: либо работает обычный код, либо код, обрабатывающий прерывание, но вместе — никогда. Поэтому там не нужны были блокировки, мьютексы и т.д. А что происходит в другими процессорами/ядрами когда на одном из них срабатывает прерывание? Они тоже останавливаются?Судя по коду, который вы привели, указатель на память dlg умрёт вместе с функций. Ведь функция имеет тип void (dlg не возвращается), переданный ей список параметров пуст (dlg не сохранятся через переданный указатель), код не пишет в статическую память (dlg не сохраняется вовне, в глобальной переменной). Следовательно, dlg умрёт вместе с родительской функций. А вот память, на которую ссылается dlg, останется жить. Имеет место быть классическая «утечка памяти»: выделенная память не уничтожается вместе с указателем. Это тот случай, который хороший язык программирования должен предотвратить: надо либо передать dlg куда-то вовне, либо вызвать delete. Там создается новый поток, и он будет "жить" не зависимо от родительской функции, и вызов delete произойдет уже из нового потока. Утечки не будет, ведь dlg куда то вовне передается, только уже в другом потоке исполнения. Добавить свой отзыв
Написать автору можно на электронную почту mail(аt)compiler.su
|