чому обсяг пам'яті стека настільки обмежений?


85

Коли ви розподіляєте пам'ять у купі, єдиним обмеженням є вільна оперативна пам'ять (або віртуальна пам'ять). Це робить Gb пам'яті.

То чому розмір стека настільки обмежений (близько 1 Мб)? Яка технічна причина заважає вам створювати по-справжньому великі об’єкти в стеку?

Оновлення : Мої наміри можуть бути незрозумілими, я не хочу розподіляти величезні об’єкти в стеку, і більший стек мені не потрібен . Це питання - лише чиста цікавість.


Чому було б практично створювати великі об’єкти на купі? (Ланцюжки викликів зазвичай йдуть на стек.)
Макото,

4
Я думаю, що реальна відповідь є простішою, ніж більшість відповідей: "тому що так ми завжди робили, і до цього часу все було гаразд, так навіщо змінюватися?"
Джеррі Коффін,

@JerryCoffin Чи читали ви будь-яку з опублікованих досі відповідей? Існує більше розуміння цього питання.
user1202136

3
@ user1202136: Я їх усі прочитав - але люди здогадуються, і я припускаю, що багато факторів, на які вони посилаються, ймовірно, навіть не враховувались при прийнятті оригінальних рішень з цього питання. Для формулювання фрази: "іноді сигара - це лише сигара".
Джеррі Коффін,

3
"Наскільки великим слід зробити стек за замовчуванням?" "О, я не знаю, скільки ниток ми можемо запустити?" "Це вибухає десь над K" "Добре, тоді ми будемо називати це 2K, у нас є 2 гіга віртуальних, то як щодо 1 мега?" "Так, добре, яке наступне випуск?"
Мартін Джеймс,

Відповіді:


46

Моя інтуїція полягає в наступному. Керувати стеком не так просто, як купою. Стек потрібно зберігати в постійних місцях пам'яті. Це означає, що ви не можете випадковим чином розподілити стек за необхідністю, але для цього вам потрібно принаймні зарезервувати віртуальні адреси. Чим більше розмір зарезервованого віртуального адресного простору, тим менше потоків ви можете створити.

Наприклад, 32-розрядна програма, як правило, має віртуальний адресний простір 2 Гб. Це означає, що якщо розмір стека становить 2 МБ (за замовчуванням у pthreads), ви можете створити максимум 1024 потоки. Це може бути невеликим для таких програм, як веб-сервери. Збільшення розміру стека, скажімо, до 100 МБ (тобто, ви зарезервуєте 100 МБ, але не обов’язково відразу ж виділите 100 МБ для стека), обмежило б кількість потоків приблизно до 20, що може бути обмеженням навіть для простих графічних програм.

Цікавим питанням є те, чому ми досі маємо цей ліміт на 64-розрядні платформи. Я не знаю відповіді, але я припускаю, що люди вже звикли до деяких "найкращих практик стеку": будьте обережні, щоб розподілити величезні об'єкти в купі і, якщо потрібно, збільшити розмір стека вручну. Тому ніхто не знайшов корисним додавати "величезну" підтримку стека на 64-розрядні платформи.


Багато 64-розрядних машин мають лише 48-розрядні адреси (надається великий коефіцієнт посилення понад 32-розрядні, але все ще обмежений). Навіть маючи додатковий простір, вам доведеться турбуватися про те, як здійснюється резервування щодо таблиць сторінок - тобто завжди є додаткові витрати, якщо ви маєте більше місця. Можливо, так само дешево, якщо не дешевше, виділити новий сегмент (mmap) замість того, щоб резервувати величезні простори стека для кожного потоку.
edA-qa mort-ora-y

4
@ edA-qamort-ora-y: Ця відповідь не говорить про розподіл , вона говорить про резервування віртуальної пам'яті , яке майже безкоштовне, і, звичайно, набагато швидше, ніж mmap.
Мукінг качка

33

Один аспект, про який ще ніхто не згадував:

Обмежений розмір стека - це механізм виявлення помилок та стримування.

Як правило, основною роботою стека в C і C ++ є відстеження стека викликів та локальних змінних, і якщо стек росте поза межами, це майже завжди є помилкою в дизайні та / або поведінці програми .

Якщо стеку дозволити довільно збільшуватися, ці помилки (як нескінченна рекурсія) будуть вловлюватися дуже пізно, лише після того, як ресурси операційної системи будуть вичерпані. Цьому запобігає встановлення довільного обмеження розміру стека. Фактичний розмір не настільки важливий, крім того, що він досить малий, щоб запобігти погіршенню системи.


У вас може бути схожа проблема з виділеними об'єктами (оскільки якийсь спосіб замінити рекурсію - це обробляти стек вручну). Це обмеження змушує використовувати інші способи (які не є необхідними для більш безпечного / простішого / ..) (Зверніть увагу на кількість зауважень щодо реалізації списку (іграшкових) std::unique_ptrдля написання деструктора (і не покладаючись на розумний вказівник)).
Jarod42

15

Це просто розмір за замовчуванням. Якщо вам потрібно більше, ви можете отримати більше - найчастіше, кажучи лінкеру виділити додатковий простір стека.

Недоліком великих стеків є те, що якщо ви створюєте багато ниток, їм знадобиться по одному стеку. Якщо всі стеки виділяють кілька МБ, але не використовують його, простір буде витрачено даремно.

Ви повинні знайти належний баланс для вашої програми.


Деякі люди, як @BJovke, вважають, що віртуальна пам'ять по суті безкоштовна. Це правда, що вам не потрібно мати фізичну пам’ять, яка підтримує всю віртуальну пам’ять. Ви повинні мати можливість принаймні видавати адреси у віртуальну пам’ять.

Однак на типовому 32-розрядному ПК розмір віртуальної пам'яті збігається з розміром фізичної пам'яті - оскільки ми маємо лише 32 біти для будь-якої адреси, віртуальної чи ні.

Оскільки всі потоки в процесі мають однаковий адресний простір, вони повинні розділити його між собою. А після того, як операційна система взяла свою участь, для програми залишається "лише" 2-3 ГБ. І цей розмір є обмеженням як для фізичної, так і для віртуальної пам’яті, оскільки адрес просто більше немає.


Найбільша проблема потоків полягає в тому, що ви не можете легко сигналізувати об'єкти стека в інші потоки. Або потік-виробник повинен синхронно чекати, поки споживчий потік звільнить об’єкт, або потрібно зробити дорогі та генеруючі суперечки глибокі копії.
Мартін Джеймс,

2
@MartinJames: Ніхто не каже, що всі об’єкти повинні бути у стеку, ми обговорюємо, чому розмір стека за замовчуванням невеликий.
Мукінг качка

Простір не буде витрачено даремно, розмір стека - це просто резервування безперервного віртуального адресного простору. Отже, якщо ви встановите розмір стека в 100 МБ, обсяг оперативної пам'яті, який буде фактично використаний, залежить від споживання стека в потоках.
BJovke

1
@BJovke - Але віртуальний адресний простір все одно буде використаний. У 32-розрядному процесі це обмежується кількома ГБ, тому просто резервування 20 * 100 МБ викличе у вас проблеми.
Бо Перссон

7

З одного боку, стек є безперервним, тому, якщо ви виділяєте 12 МБ, ви повинні видалити 12 МБ, коли ви хочете опуститися нижче того, що ви створили. Також переміщення предметів навколо стає набагато складнішим. Ось приклад із реального світу, який може полегшити розуміння:

Скажімо, ви складаєте коробки по кімнаті. Що простіше в управлінні:

  • складаючи ящики будь-якої ваги один на одного, але коли вам потрібно дістати щось на дні, вам доведеться скасувати всю свою купу. Якщо ви хочете вийняти предмет з купи і передати його комусь іншому, ви повинні зняти всі ящики і перенести коробку на купу іншої людини (лише для стека)
  • Ви кладете всі свої коробки (за винятком справді невеликих коробок) в спеціальну зону, де ви не складаєте речі поверх інших речей, а записуєте, де ви кладете їх на аркуш паперу (вказівник) і кладете папір на купу. Якщо вам потрібно передати коробку комусь іншому, просто передайте їм аркуш паперу з вашої купи або просто дайте їм ксерокопію паперу та залиште оригінал там, де він був у вашій купі. (Стек + купа)

Ці два приклади є грубими узагальненнями, і є деякі моменти, які явно помиляються в аналогії, але це досить близько, що, сподіваємось, допоможе вам побачити переваги в обох випадках.


@MooingDuck Так, але ви працюєте у віртуальній пам’яті у своїй програмі. Якщо я введу підпрограму, покладу щось у стек, а потім повернуся з підпрограми, мені потрібно буде або де-розподілити, або перемістити об’єкт, який я створив, перш ніж я зможу розкрутитися стек, щоб повернутися туди, звідки я прийшов.
Скотт Чемберлен,

1
хоча мій коментар був пов’язаний з неправильним тлумаченням (і я його видалив), я все ще не погоджуюся з цією відповіддю. Видалення 12 Мб з верхньої частини стека - це буквально один код роботи. Це в основному безкоштовно. Крім того, компілятори можуть і обманюють правило "стека", тому ні, їм не потрібно копіювати / переміщувати об'єкт, перш ніж розмотувати, щоб повернути його. Тому я вважаю, що ваш коментар також невірний.
Мукінг качка

Ну, як правило, не має великого значення те, що для вивільнення 12 Мб потрібно один код операції в стеку понад 100 у купі - це, мабуть, нижче рівня шуму фактичної обробки буфера 12 Мб. Якщо компілятори хочуть обдурити, коли помічають, що повертається смішно великий об’єкт (наприклад, переміщаючи SP перед викликом, щоб зробити простір об’єктів частиною стека абонентів), тоді це нормально, TBH, розробники, які повертають такі Об'єкти (а не покажчики / посилання) дещо викликають проблеми з програмуванням.
Мартін Джеймс,

@MartinJames: Специфікація С ++ також говорить, що функція зазвичай може вкладати дані безпосередньо в буфер призначення, а не використовувати тимчасовий, тому, якщо ви будете обережні, немає жодних накладних витрат на повернення буфера 12 МБ за значенням.
Мукаюча качка

4

Подумайте про стек у порядку, близькому до далекого. Регістри близькі до центрального процесора (швидко), стек трохи далі (але все ще відносно близько) і купа далеко (повільний доступ).

Стек живе на купі курсу, але все ж, оскільки він використовується постійно, він, ймовірно, ніколи не залишає кеш (и) центрального процесора, що робить його швидшим, ніж просто середній доступ до купи. Це є причиною зберігати стек розумних розмірів; щоб максимально зберегти його в кешу. Виділення великих об’єктів стека (можливо, автоматичне змінення розміру стека в міру переповнення) суперечить цьому принципу.

Отже, це хороша парадигма для продуктивності, а не просто залишки старих часів.


4
Хоча я впевнений, що кешування відіграє важливу роль у штучному зменшенні розміру стека, я повинен виправити вас із твердженням "стек живе в купі". І стек, і купа зберігаються в пам'яті (віртуально чи фізично).
Себастьян Хохас,

Як "близький чи далекий" пов'язаний із швидкістю доступу?
Minh Nghĩa

@ MinhNghĩa Ну, змінні в оперативній пам’яті кешуються в пам’яті L2, потім це кешується в пам’яті L1, а потім навіть ті, що кешуються в регістрах. Доступ до оперативної пам'яті повільний, до L2 швидший, L1 все одно швидший, а реєстрація найшвидша. Я маю на увазі, що OP мав на увазі, що до змінних, що зберігаються в стеку, передбачається швидкий доступ, тому центральний процесор намагатиметься якнайкраще утримувати змінні стека поблизу, отже, ви хочете зробити його маленьким, отже, центральний процесор може швидше отримати доступ до змінних.
157 239н.

1

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

"Алгоритми" Седжвіка має кілька хороших прикладів "видалення" рекурсії з таких рекурсивних алгоритмів, як QuickSort, шляхом заміни рекурсії на ітерацію. Насправді алгоритм все ще рекурсивний, і він все ще існує як стек, але ви виділяєте стек сортування в купі, а не використовуючи стек часу виконання.

(Я віддаю перевагу другому виданню з алгоритмами, поданими на мові Паскаль. Його можна використовувати за вісім доларів.)

Ще один спосіб подивитися на це - якщо ви вважаєте, що вам потрібен великий стек, ваш код неефективний. Існує кращий спосіб, який використовує менше стека.


-8

Я не думаю, що існує якась технічна причина, але це був би дивний додаток, який щойно створив лише один величезний супер-об'єкт у стеку. Об'єктам стеку не вистачає гнучкості, що стає більш проблематичним із збільшенням розміру - ви не можете повернутися, не знищивши їх, і ви не можете поставити їх у чергу в інші потоки.


1
Ніхто не каже, що всі об’єкти повинні бути у стеку, ми обговорюємо, чому розмір стека за замовчуванням невеликий.
Мукінг качка

Це не мало! Скільки всього функціональних викликів вам доведеться пройти, щоб використати 1 МБ стека? Значення за замовчуванням у будь-якому випадку легко змінюються у компонувальнику, і тому ми залишаємося з "чому використовувати стек замість купи?"
Мартін Джеймс,

3
один виклик функції. int main() { char buffer[1048576]; } Це дуже поширена проблема для початківців. Звичайно, існує простий спосіб вирішення проблеми, але чому нам слід обходити розмір стека?
Мукінг качка

Ну, з одного боку, я не хотів би, щоб 12 МБ (або, дійсно, 1 МБ) вимоги до стеку були завдані стеку кожного потоку, який викликає порушену функцію. Тим не менш, я повинен погодитись, що 1 Мб трохи скупий. Я був би радий за замовчуванням 100 Мб, врешті-решт, ніщо не заважає мені зменшити його до 128 КБ точно так само, як ніщо не зупиняє інших розробників, що його збільшують.
Мартін Джеймс,

1
Чому б вам не хотілося нанести 12 МБ стека на вашу нитку? Єдина причина цього полягає в тому, що стоси невеликі. Це рекурсивний аргумент.
Мукінг качка
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.