Чи може бути більш ефективним для систем взагалі відмовитися від стеків і просто використовувати Heap для управління пам’яттю?


14

Мені здається, що все, що можна зробити зі стеком, можна зробити з купою, але не все, що можна зробити з купою, можна зробити зі стеком. Це правильно? Тоді для простоти, і навіть якщо ми втрачаємо невелику продуктивність при певних робочих навантаженнях, чи не може бути краще просто піти з одним стандартом (тобто купою)?

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


1
У C і C ++ потрібно чітко розташувати пам'ять, виділену на купі. Це не простіше.
user16764

Я використовував реалізацію C #. Профілюванням профілю було встановлено, що об'єкти стека виділяються у купі, схожій на жахливий збір сміття. Моє рішення? Перемістіть усе можливе (наприклад, змінні циклу, тимчасові змінні тощо) у стійку пам’ять. Змусив програму з'їсти 10-кратну оперативну пам’ять і працювати з 10-кратною швидкістю.
imallett

@IanMallett: Я не розумію вашого пояснення проблеми та рішення. У вас десь є посилання з додатковою інформацією? Зазвичай я вважаю, що розподіл на основі стека є швидшим.
Френк Хілеман

@FrankHileman основною проблемою було таке: реалізація C #, яку я використовував, мала надзвичайно низьку швидкість збору сміття. "Рішення" полягало в тому, щоб зробити всі змінні стійкими, щоб під час виконання жодних операцій з пам'яттю не відбувалося. Нещодавно я писав думку про розвиток C # / XNA взагалі, який також обговорює деякий контекст.
imallett

@IanMallett: дякую. Як колишній розробник C / C ++, який в основному використовує C # в наші дні, мій досвід був зовсім іншим. Я вважаю, що найбільшою проблемою є бібліотеки. Здається, платформа XBox360 була напівзапечена для розробників .net. Зазвичай, коли у мене проблеми з GC, я переходжу на об'єднання. Це допомагає.
Френк Хілеман

Відповіді:


30

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

Повністю позбутися стека заради спрощення мови програмування - це неправильний спосіб ІМО - кращим підходом було б абстрагувати відмінності, нехай компілятор розібрається, який тип пам’яті використовувати, тоді як програміст збирає разом конструкти на рівні, ближчі до того, як люди думають, - і насправді це роблять мови високого рівня, такі як C #, Java, Python тощо. Вони пропонують майже ідентичний синтаксис для виділених купою об'єктів та присвоєних стеком примітивів ('типи посилань' проти 'типи значень' у .NET lingo), або повністю прозорі, або з кількома функціональними відмінностями, які ви повинні розуміти, щоб використовувати мову правильно (але ви насправді не повинні знати, як стек і купа працюють всередині).


2
ЯК ЦЕ БУЛО ДОБРО :) По-справжньому лаконічним та інформативним для початківця!
Темний тамплієр

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

@Patrick Hughes: Так, але Купа також знаходиться в апараті, чи не так?
Темний тамплієр

@Dark Що Патрік, ймовірно, хоче сказати, це те, що такі архітектури, як x86, мають спеціальні регістри для управління стеком та спеціальні інструкції, щоб щось поставити або видалити з / зі стека. Це робить це досить швидко.
FUZxxl

3
@Donal стипендіатів: Все вірно. Але справа в тому, що стеки і купи мають і сильні, і слабкі місця, і використання їх відповідно дасть найефективніший код.
tdammers

8

Простіше кажучи, стек - це не маленька продуктивність. Це в сотні чи тисячі разів швидше, ніж купа. Крім того, більшість сучасних машин мають апаратну підтримку стека (наприклад, x86), і функціональні можливості апаратного забезпечення, наприклад, стек викликів неможливо видалити.


Що ви маєте на увазі, говорячи про те, що сучасні машини мають апаратну підтримку для стека? Сам стек уже є в апаратному забезпеченні, чи не так?
Темний тамплієр

1
x86 має спеціальні регістри та інструкції по роботі зі стеком. x86 не підтримує купи - такі речі створюються ОС.
Паббі

8

Ні

Площа стека в C ++ неймовірно швидка в порівнянні. Я маю на увазі, що жоден досвідчений розробник C ++ не може відключити цю функціональність.

З C ++ у вас є вибір і ви маєте контроль. Дизайнери не були особливо схильні вводити функції, які додавали значного часу або простору для виконання.

Здійснюючи цей вибір

Якщо ви хочете створити бібліотеку або програму, яка вимагає, щоб кожен об'єкт був розподілений динамічно, ви можете зробити це за допомогою C ++. Це виконується порівняно повільно, але ви могли б мати цю "модульність". Для решти нас модульність завжди є необов’язковою, запроваджуйте її за потребою, оскільки обидві потрібні для гарної / швидкої реалізації.

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

Є й інші мови, які вимагають, щоб сховище для кожного об’єкта створювалося в купі; він досить повільний, такий, що компрометує проекти (реальні програми) таким чином, що гірше, ніж вчитися обом (ІМО).

Обидва є важливими, і C ++ дозволяє ефективно використовувати енергію для кожного конкретного сценарію. Сказавши це, мова C ++ може не бути ідеальною для вашого дизайну, якщо ці фактори у вашій ОП важливі для вас (наприклад, читайте на мовах вищого рівня).


Купа насправді така ж швидкість, як стек, але не має спеціалізованої апаратної підтримки для розподілу. З іншого боку, існують способи значно прискорити купу (при дотриманні ряду умов, які роблять їх методиками лише для експертів).
Дональні стипендіати

@DonalFellows: апаратне забезпечення для стеків не має значення. Що важливо, це знати, що кожного разу, коли щось звільнено, він може випустити все, що виділяється після цього. Деякі мови програмування не мають купи, які можуть самостійно звільняти об'єкти, а натомість мають лише метод "безкоштовно все, що виділяється після".
supercat

6

Тоді для простоти, і навіть якщо ми втрачаємо невелику продуктивність при певних робочих навантаженнях, чи не може бути краще просто піти з одним стандартом (тобто купою)?

Насправді хіт продуктивності, ймовірно, буде значним!

Як зазначають інші, стеки є надзвичайно ефективною структурою для управління даними, що підкоряються LIFO (останнім у першому випадку) правилам. Розподіл / звільнення пам'яті в стеку зазвичай є лише зміною регістра в ЦП. Зміна регістра майже завжди є однією з найшвидших операцій, які може виконувати процесор.

Купа, як правило, досить складна структура даних, і для розподілу / звільнення пам'яті знадобиться багато інструкцій, щоб виконати всі пов'язані з ними бухгалтерії. Ще гірше, що в загальних реалізаціях кожен виклик роботи з купою має потенціал призвести до виклику операційної системи. Виклики операційної системи дуже трудомісткі! Програма зазвичай повинна переходити з режиму користувача в режим ядра, і коли це станеться, операційна система може вирішити, що інші програми мають більш нагальні потреби, і що програмі потрібно буде почекати.


5

Симула використовувала купу для всього. Якщо все помістити в купу, це завжди викликає ще один рівень непрямості для локальних змінних, і це надає додатковий тиск на збирач сміття (ви повинні врахувати, що збирачі сміття тоді справді засмоктувались). Частково тому Б'ярн винайшов C ++.


Так що в основному C ++ також використовує купу також?
Темний тамплієр

2
@Dark: Що? Ні. Відсутність стека в Simula була натхненником зробити це краще.
fredoverflow

Ах, я бачу, що ти маєш на увазі зараз! Дякую +1 :)
Темний тамплієр

3

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


2

"ефективний" з точки зору написання коду, можливо, але, безумовно, не з точки зору вашої ефективності програмного забезпечення. Виділення стеків по суті безкоштовні (потрібно лише кілька інструкцій машин для переміщення покажчика стека та резервування місця в стеку для локальних змінних).

Оскільки розподіл стека займає майже не час, розподіл навіть на дуже ефективній купі буде в 100 к (якщо не 1 М +) у рази повільніше.

Тепер уявіть, скільки локальних змінних та інших структур даних використовує типовий додаток. Кожне маленьке "я", яке ви використовуєте як лічильник циклу, виділяється в мільйон разів повільніше.

Звичайно, якщо обладнання досить швидко, ви можете написати програму, яка використовує лише купи. Але тепер уявіть, який саме додаток ви можете написати, якби ви скористалися грою і використовували те саме обладнання.


Коли ви говорите "уявіть, скільки локальних змінних та інших структур даних використовує типова програма", які ще структури даних ви конкретно маєте на увазі?
Темний тамплієр

1
Чи є значення "100k" та "1M +" якось науковими? Або це просто спосіб сказати "багато"?
Бруно Рейс

@Bruno - IMHO цифри 100K та 1M, які я використав, - це фактично консервативна оцінка, щоб довести свою точку. Якщо ви знайомі з VS та C ++, напишіть програму, яка виділяє 100 байт на стеку, і напишіть ту, яка виділяє 100 байт на купі. Потім перейдіть до подання на розбирання та просто підрахуйте кількість інструкцій по збірці, які приймає кожен розподіл. Операції купи - це декілька функціональних викликів у Windows DLL, є відра та пов'язані списки, а потім є злиття та інші алгоритми. За допомогою стека він може зводитися до однієї інструкції по збірці "add esp, 100" ...
DXM

2
"100 к (якщо не 1 М +) разів повільніше"? Це зовсім трохи перебільшено. Нехай це буде на два порядки повільніше, можливо, на три, але це все. Принаймні, мій Linux здатний зробити 100M розподілу купи (+ деякі навколишні інструкції) менше ніж за 6 секунд на ядрі i5, що не може бути більше кількох сотень інструкцій на розподіл - насправді це майже напевно менше. Якщо це на шість порядків повільніше, ніж стек, з реалізацією купи ОС в основному не так. Звичайно, з Windows багато що не так, але це ...
Продовжившись

1
Модератори, ймовірно, збираються вбити всю цю нитку коментарів. Тож ось угода, я визнаю, що фактичні цифри були витягнуті з моєї ...., але погодьмося, коефіцієнт дійсно, дуже великий і не роби більше коментарів :)
DXM

2

Можливо, вас зацікавить "Збір сміття швидкий, але стек швидше".

http://dspace.mit.edu/bitstream/handle/1721.1/6622/AIM-1462.ps.Z

Якщо я правильно прочитав, ці хлопці змінили компілятор C, щоб виділити "стеки кадри" на купі, а потім використовувати сміття для розподілу кадрів, а не вискакувати стек.

"Кадри стека", виділені стеком, вирішально перевершують виділені "купи" стеки.


1

Як буде стек викликів працювати на купі? По суті, вам доведеться виділити стек на купі в кожній програмі, так чому б вам не зробити апаратне забезпечення OS + зробити це для вас?

Якщо ви хочете, щоб речі були справді простими та ефективними, просто дайте користувачеві шматок пам’яті та дозвольте їм вирішити це. Звичайно, ніхто не хоче реалізовувати все своє, і саме тому у нас є стек і купа.


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

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

@Ingo Насправді не в жодному змістовному сенсі. Звичайно, ОС ініціалізує розділ пам'яті, який традиційно називається "стек", і там буде вказаний реєстр. Але функції в мові джерела не закінчуються, представлені у вигляді кадру стека в порядку виклику. Виконання функції повністю представлено маніпулюванням структурами даних у купі. Навіть не використовуючи оптимізацію останнього дзвінка, "переповнювати стек" неможливо. Це я маю на увазі, коли кажу, що немає нічого принципового щодо "стека викликів".
Бен

Я говорю не про функції мови-джерела, а про функції в інтерпретаторі (або будь-якому іншому), які фактично виконують скорочення графіків. Їм знадобиться стопка. Це очевидно, оскільки сучасне обладнання не робить скорочення графіків. Отже, ваш алгоритм скорочення графіків в кінцевому підсумку відображається на машинній оді, і, мабуть на заклад, серед них є виклики підпрограми. QED.
Інго

1

І стек, і купа потрібні. Вони використовуються в різних ситуаціях, наприклад:

  1. Виділення купи має обмеження, що sizeof (a [0]) == sizeof (a [1])
  2. Розподіл стеків має обмеження, що розмір a (a) є константа часу збирання
  3. Виділення купи може робити петлі, графіки тощо, складні структури даних
  4. Розподіл стеків може робити дерева розміром за компіляцією
  5. Купа вимагає відстеження права власності
  6. Розподіл та розміщення стеків відбувається автоматично
  7. Пам'ять купи може легко передаватися з однієї області в іншу за допомогою покажчиків
  8. Пам'ять стека є локальною для кожної функції, і об’єкти потрібно перемістити у верхню область, щоб продовжити їхнє життя (або зберігати всередині об'єктів замість внутрішніх функцій членів)
  9. Купа погана для продуктивності
  10. Стек досить швидкий
  11. Купи об'єктів повертаються з функцій за допомогою покажчиків, які беруть на себе право власності. Або shared_ptrs.
  12. Об'єкти стека повертаються з функцій за допомогою посилань, які не беруть на себе право власності.
  13. Купа вимагає відповідності кожного нового правильному виду видалення або видалення []
  14. Об'єкти стека використовують списки ініціалізації RAII та конструктора
  15. Купи об'єктів можуть бути ініціалізовані в будь-якій точці функції та не можуть використовувати параметри конструктора
  16. Об'єкти стека використовують параметри конструктора для ініціалізації
  17. Heap використовує масиви, а розмір масиву може змінюватися під час виконання
  18. Стек призначений для окремих об'єктів, а розмір фіксується на час компіляції

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


1

Сучасні комп’ютери мають кілька шарів кеш-пам'яті на додаток до великої, але повільної основної системи пам'яті. Можна зробити десятки доступу до найшвидшого кеш-пам'яті за час, необхідний для читання або запису одного байта з основної системи пам'яті. Таким чином, отримати доступ до однієї локації в тисячу разів набагато швидше, ніж отримати доступ до 1000 (або навіть 100) незалежних місць один раз у кожному. Оскільки більшість програм неодноразово виділяють і розміщують невеликий обсяг пам’яті біля верхньої частини стека, локації у верхній частині стека звикають і повторно використовують величезну кількість, так що переважна більшість (99% + у типовому додатку) Доступ до стеку може бути оброблений за допомогою кеш-пам'яті.

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.