Как C и C ++ хранят большие объекты в стеке? - PullRequest
18 голосов
/ 10 января 2009

Я пытаюсь выяснить, как C и C ++ хранят большие объекты в стеке. Обычно размер стека равен целому числу, поэтому я не понимаю, как там хранятся объекты большего размера. Они просто занимают несколько стековых "слотов"?

Ответы [ 11 ]

30 голосов
/ 10 января 2009

Стек и куча не такие разные, как вы думаете!


Правда, некоторые операционные системы имеют ограничения по стеку. (Некоторые из них также имеют неприятные ограничения кучи!)

Но это уже не 1985 год.

В эти дни я использую Linux!

Мое значение по умолчанию Размер стека ограничено 10 МБ. Мое значение по умолчанию heapsize не ограничено. Это довольно тривиально, чтобы ограничить размер стека. (* кашель * [tcsh] неограниченный размер стека * кашель *. Или setrlimit () .)

Самые большие различия между стеком и кучей :

  1. стек выделения просто смещают указатель (и, возможно, выделяют новые страницы памяти, если стек стал достаточно большим). Heap должен искать в своих структурах данных, чтобы найти подходящий блок памяти. (И, возможно, выделять новые страницы памяти тоже.)
  2. стек выходит из области видимости, когда заканчивается текущий блок. Heap выходит из области действия при вызове delete / free.
  3. Куча может быть фрагментирована. Стек никогда не фрагментируется.

В Linux и стек и куча управляются через виртуальную память.

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

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

( new вызовет конструктор, который предположительно будет использовать эти страницы памяти ...)


Вы можете уничтожить систему ВМ, создав и уничтожив крупные объекты либо в стеке , либо в куче . Это зависит от вашей ОС / компилятора, может ли память быть восстановлена ​​системой. Если это не исправлено, куча могла бы повторно использовать это. (Предполагая, что он не был повторно перенаправлен другим malloc () за это время.) Аналогично, если стек не будет восстановлен, он будет просто повторно использован.

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


Из всех этих вещей Я больше всего беспокоюсь о фрагментации памяти !

Продолжительность жизни (когда она выходит за рамки) всегда является решающим фактором.

Но когда вы запускаете программы в течение длительных периодов времени, фрагментация создает постепенно увеличивающийся объем памяти. Постоянный обмен в конечном итоге убивает меня!




ИЗМЕНЕНО В ДОБАВЛЕНИИ:


Чувак, я был избалован!

Что-то здесь просто не складывалось ... Я тоже подумала, что * я * чертовски далеко от базы. Или все остальные были. Или, скорее, оба. Или, может быть, ни то, ни другое.

Каким бы ни был ответ, я должен был знать, что происходит!

... Это будет долго. Терпи меня ...


Я провел большую часть последних 12 лет, работая под Linux. И примерно за 10 лет до этого под разными вкусами Unix. Мой взгляд на компьютеры несколько предвзят. Я был избалован!

Я немного поработал с Windows, но недостаточно, чтобы говорить авторитетно. Как ни трагично, но и с Mac OS / Darwin ... Хотя Mac OS / Darwin / BSD достаточно близки, чтобы некоторые из моих знаний были перенесены.


При использовании 32-разрядных указателей вам не хватает адресного пространства в 4 ГБ (2 ^ 32).

С практической точки зрения, STACK + HEAP в сочетании обычно ограничен где-то между 2-4 ГБ, так как другие вещи должны отображаться там.

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


В Linux / Unix / MacOS / Darwin / BSD вы можете искусственно ограничить HEAP или STACK любыми произвольными значениями, которые вы хотите во время выполнения. Но в конечном итоге существует жесткое ограничение системы.

Это различие (в tcsh) "limit" vs "limit -h" . Или (в bash) "ulimit -Sa" vs "ulimit -Ha" . Или, программно, из rlim_cur против rlim_max в struct rlimit .


Теперь перейдем к самой интересной части. В отношении Код Мартина Йорка . (Спасибо Мартин ! Хороший пример. Всегда хорошо пробовать!)

Мартин предположительно работает на Mac. (Довольно недавний. Его компилятор новее моего!)

Конечно, его код не будет работать на его Mac по умолчанию. Но он будет работать нормально, если он сначала вызовет "unlimit stacksize" (tcsh) или "ulimit -Ss unlimited" (bash).


СЕРДЦЕ МАТЕРИИ:


Тестирование на древнем (устаревшем) блоке ядра Linux RH9 2.4.x с выделением большого количества STACK ИЛИ HEAP , либо один из них сам по себе завершается от 2 до 3 ГБ. (К сожалению, RAM + SWAP машины занимает чуть меньше 3,5 ГБ. Это 32-битная ОС. И это НЕ единственный процесс, который выполняется. Мы справляемся с тем, что имеем ...)

Так что на самом деле нет никаких ограничений на размер STACK против HEAP под Linux, кроме искусственных ...


НО:


На Mac жесткое ограничение размера стека составляет 65532 килобайт . Это связано с тем, как что-то заложено в память.


Обычно вы думаете об идеализированной системе как имеющей STACK на одном конце адресного пространства памяти, HEAP на другом, и они строятся навстречу друг другу. Когда они встречаются, вам не хватает памяти.

Mac, кажется, прикрепляют свои Общие системные библиотеки между ними с фиксированным смещением, ограничивающим обе стороны. Вы по-прежнему можете запускать код Мартина Йорка с "неограниченным размером стека", поскольку он выделяет только около 8 МБ (<64 МБ) данных. <strong>Но у него кончится STACK задолго до того, как у него кончится HEAP .

Я нахожусь на Linux. Я не буду. Прости, малыш. Вот никель. Возьми себе лучшую ОС.

Есть обходные пути для Mac. Но они становятся уродливыми и грязными и требуют настройки параметров ядра или компоновщика.

В конечном итоге, если Apple не сделает что-то действительно глупое, 64-битные адресные пространства сделают всю эту проблему с ограничением стека устаревшей когда-то в реальном времени.


Переход к фрагментации:


Каждый раз, когда вы помещаете что-то в STACK , оно добавляется в конец И он удаляется (откатывается) при выходе из текущего блока.

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

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

Фрагментация памяти НЕ причина избегать хранения HEAP . Это просто то, что нужно знать, когда вы кодируете.


Что вызывает SWAP THRASHING :


  • Если у вас уже есть большое количество выделенной / используемой кучи.
  • Если у вас разбросано много фрагментированных отверстий.
  • А если у вас большое количество небольших выделений.

Затем вы можете получить большое количество переменных, которые используются в небольшой локализованной области кода, которые разбросаны по множеству страниц виртуальной памяти. (Как вы используете 4 байта на этой странице 2k, и 8 байтов на этой странице 2k, и так далее для целого ряда страниц ...)

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

С другой стороны, если бы эти небольшие выделения были сделаны на STACK , все они были бы расположены в непрерывном отрезке памяти. Меньше страниц памяти VM должно быть загружено. (4 + 8 + ... <2k за победу.) </p>

Sidenote: Моя причина для привлечения внимания к этому исходит от некоего инженера-электрика, которого я знал, который настаивал на том, чтобы все массивы были размещены в HEAP. Мы делали математику для графики. * LOT * из 3 или 4 элементов массива. Управление только новым / удалением было кошмаром. Даже отвлекшись на занятиях, это вызвало горе!


Следующая тема. Заправка:


Да, потоки по умолчанию ограничены очень маленькими стеками.

Вы можете изменить это с помощью pthread_attr_setstacksize (). Хотя в зависимости от вашей реализации потоков, если несколько потоков совместно используют одно и то же 32-разрядное адресное пространство, большие отдельные стеки для каждого потока будут проблемой! Там просто не так много места! Опять же, переход на 64-битные адресные пространства (ОС) поможет.

pthread_t       threadData;
pthread_attr_t  threadAttributes;

pthread_attr_init( & threadAttributes );
ASSERT_IS( 0, pthread_attr_setdetachstate( & threadAttributes,
                                             PTHREAD_CREATE_DETACHED ) );

ASSERT_IS( 0, pthread_attr_setstacksize  ( & threadAttributes,
                                             128 * 1024 * 1024 ) );

ASSERT_IS( 0, pthread_create ( & threadData,
                               & threadAttributes,
                               & runthread,
                               NULL ) );

Относительно Мартина Йорка Рамки стека:


Возможно, мы с тобой думаем о разных вещах?

Когда я думаю о кадре стека , я думаю о стеке вызовов. Каждая функция или метод имеет свой собственный стековый фрейм , состоящий из адреса возврата, аргументов и локальных данных.

Я никогда не видел никаких ограничений на размер стекового фрейма . Существуют ограничения на STACK в целом, но это все стековых фреймов вместе взятых.

На Wiki есть хорошая диаграмма и обсуждение стековых фреймов .


На последнем примечании:


В Linux / Unix / MacOS / Darwin / BSD можно программно изменить максимальные ограничения STACK , а также limit (tcsh) или ulimit (Баш):

struct rlimit  limits;
limits.rlim_cur = RLIM_INFINITY;
limits.rlim_max = RLIM_INFINITY;
ASSERT_IS( 0, setrlimit( RLIMIT_STACK, & limits ) );

Только не пытайтесь установить его на INFINITY на Mac ... И измените его, прежде чем пытаться его использовать. ;-)


Дополнительное чтение:



26 голосов
/ 10 января 2009

Стек - это кусок памяти. Указатель стека указывает на вершину. Значения могут быть помещены в стек и извлечены для их извлечения.

Например, если у нас есть функция, которая вызывается с двумя параметрами (размером 1 байт и размером 2 байта; просто предположим, что у нас 8-битный ПК).

Оба помещаются в стек, это перемещает указатель стека вверх:

03: par2 byte2
02: par2 byte1
01: par1

Теперь вызывается функция и адрес возврата помещается в стек:

05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1

ОК, внутри функции у нас есть 2 локальные переменные; один из 2 байтов и один из 4. Для них зарезервирована позиция в стеке, но сначала мы сохраняем указатель стека, чтобы мы знали, где переменные начинаются с подсчета, а параметры - с обратного

11: var2 byte4
10: var2 byte3
09: var2 byte2
08: var2 byte1
07: var1 byte2
06: var1 byte1
    ---------
05: ret byte2
04: ret byte1
03: par2 byte2
02: par2 byte1
01: par1

Как видите, вы можете положить что-либо в стек, если у вас есть свободное место. И еще вы получите феномен, который дает этому сайту свое имя.

9 голосов
/ 10 января 2009
Инструкции

Push и pop обычно не используются для хранения локальных переменных стекового фрейма. В начале функции кадр стека устанавливается путем уменьшения указателя стека на количество байтов (выровненных по размеру слова), требуемых локальными переменными функции. Это выделяет необходимый объем пространства «в стеке» для этих значений. Все локальные переменные затем доступны через указатель на этот стек (ebp на x86).

5 голосов
/ 10 января 2009

Стек - это большой блок памяти, в котором хранятся локальные переменные, информация для возврата из вызовов функций и т. Д. Фактический размер стека значительно варьируется в ОС. Например, при создании нового потока в Windows размер по умолчанию составляет 1 МБ .

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

Стек не делится на куски целого размера. Это просто массив байтов. Он индексируется целым числом типа size_t (не int). Если вы создаете большой объект стека, который помещается в доступное в настоящее время пространство, он просто использует это пространство, увеличивая (или уменьшая) указатель стека.

Как уже отмечали другие, лучше использовать кучу для больших объектов, а не для стека. Это позволяет избежать проблем переполнения стека.

РЕДАКТИРОВАТЬ: Если вы используете 64-разрядное приложение, и ваша ОС и библиотеки времени выполнения хороши для вас (см. Сообщение mrree), тогда хорошо разместить большие временные объекты в стеке , Если ваше приложение 32-битное и / или ваша ОС / библиотека времени выполнения не подходит, вам, вероятно, нужно разместить эти объекты в куче.

3 голосов
/ 10 января 2009

Когда вы вводите функцию, стек увеличивается в соответствии с локальными переменными в этой функции. Учитывая largeObject класс, который использует, скажем, 400 байтов:

void MyFunc(int p1, largeObject p2, largeObject *p3)
{
   int s1;
   largeObject s2;
   largeObject *s3;
}

Когда вы вызываете эту функцию, ваш стек будет выглядеть примерно так (детали могут различаться в зависимости от соглашения о вызовах и архитектуры):

   [... rest of stack ...]
   [4 bytes for p1] 
   [400 bytes for p2]
   [4 bytes for p3]
   [return address]
   [old frame pointer]
   [4 bytes for s1]
   [400 bytes for s2]
   [4 bytes for s3]

См. x86 Calling Conventions для получения некоторой информации о том, как работает стек. В MSDN также есть несколько хороших диаграмм для нескольких различных конвенций вызова, с Образцом кода и результирующими диаграммами стека .

2 голосов
/ 10 января 2009

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

Они просто занимают несколько стеков? "слоты"?

Я собираюсь предположить, что вы просто имеете в виду что-то большее, чем целое число. Однако, как заметил кто-то другой, стек не имеет «слотов» целого размера - это просто часть памяти, и каждый байт в нем имеет свой собственный адрес. Компилятор отслеживает каждую переменную по адресу первого байта этой переменной - это значение, которое вы получите, если используете оператор address-of (&var), и значение указателя это просто этот адрес для какой-то другой переменной. Компилятор также знает, к какому типу относится каждая переменная (вы говорили об этом, когда объявляли переменную), и он знает, какой должен быть каждый тип - когда вы компилируете программу, он делает все, что нужно для вычисления, чтобы выяснить, сколько пространства переменные понадобятся при вызове функции и включают результат этого в код точки входа в функцию (кадр стека, упомянутый PDaddy).

1 голос
/ 10 января 2009

Размер стека ограничен. Обычно размер стека устанавливается при создании процесса. Каждый поток в этом процессе автоматически получает размер стека по умолчанию, если не указано иное в вызове CreateThread (). Итак, да: может быть несколько слотов стека, но каждый поток имеет только один. И они не могут быть разделены между потоками.

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

Итак, если у вас есть очень большие объекты, разместите их в куче, а не в стеке. Куча ограничена только объемом виртуальной памяти (которая на величину больше стека).

1 голос
/ 10 января 2009

В C и C ++ вы не должны хранить большие объекты в стеке, потому что стек ограничен (как вы уже догадались). Размер стека для каждого потока обычно составляет всего пару мегабайт или меньше (это можно указать при создании потока). Когда вы вызываете «new» для создания объекта, он не помещается в стек - вместо этого он помещается в кучу.

0 голосов
/ 10 января 2009

Как вы определяете большой объект? мы говорим больше или меньше размера выделенного стекового пространства?

например, если у вас есть что-то вроде этого:

void main() {
    int reallyreallybigobjectonthestack[1000000000];
}

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

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

0 голосов
/ 10 января 2009

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

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