Проблеми, характерні для мови C ++
Перш за все, немає так званого "стека" або "купи" розподілу, дорученого C ++ . Якщо ви говорите про автоматичні об'єкти в областях блоків, вони навіть не «виділяються». (BTW, тривалість автоматичного зберігання в C, безумовно, НЕ та сама, що "виділена"; остання є "динамічною" в мові C ++.) Динамічно виділена пам'ять знаходиться у вільному сховищі , не обов'язково на "купі", хоча Останнє часто ( по замовчуванню) реалізація .
Хоча згідно з семантичними правилами абстрактних машин , автоматичні об'єкти все ще займають пам’ять, відповідною реалізацією C ++ дозволяється ігнорувати цей факт, коли він може довести, що це не має значення (коли це не змінює спостережувану поведінку програми). Цей дозвіл надається правилом нібито, в ISO C ++, що також є загальним пунктом, що дозволяє застосовувати звичайні оптимізації (а в ISO C також існує майже те саме правило). Окрім правила «як би», ISO C ++ також має правила копіювання елісей щоб дозволити опускання конкретних творінь об’єктів. Таким чином, виклики конструктора та деструктора, що залучаються, опускаються. Як результат, автоматичні об'єкти (якщо такі є) у цих конструкторах та деструкторах також усуваються, порівняно з наївною абстрактною семантикою, що має на увазі вихідний код.
З іншого боку, розміщення безкоштовного магазину, безумовно, "розподіл" за дизайном. Відповідно до правил ISO C ++, таке розподіл може бути досягнуто викликом функції розподілу . Однак, оскільки ISO C ++ 14, існує нове правило (не як якщо), яке дозволяє об'єднувати глобальну функцію розподілу (тобто ::operator new
) викликів у конкретних випадках. Тож частини динамічних операцій розподілу також можуть бути неоперативними, як у випадку з автоматичними об'єктами.
Функції розподілу виділяють ресурси пам'яті. Об'єкти можна надалі розподілити на основі розподілу за допомогою розподільників. Для автоматичних об'єктів вони представлені безпосередньо - хоча базова пам'ять може бути доступна та використана для надання пам’яті іншим об'єктам (шляхом розміщення new
), але це не має великого сенсу як безкоштовний магазин, оскільки немає можливості перемістити переміщення ресурси в інших місцях.
Всі інші проблеми виходять за рамки C ++. Тим не менш, вони можуть бути все ще значущими.
Про реалізацію C ++
C ++ не виставляє змінених записів активації чи якихось першокласних продовжень (наприклад, відомих call/cc
), немає ніякого способу безпосередньо маніпулювати кадрами записів активації - там, де реалізація потребує розміщення автоматичних об'єктів. Після того, як не відбувається (не портативна) взаємодія з базовою реалізацією ("нативним" непереносним кодом, таким як вбудований код складання), опущення базового розподілу кадрів може бути досить тривіальним. Наприклад, коли викликана функція вбудована, кадри можна ефективно об'єднати в інші, тому немає можливості показати, що таке "розподіл".
Однак, як тільки дотримуються інтеропи, справи стають складними. Типова реалізація C ++ дозволить виявити можливість взаємодії на ISA (архітектура набору інструкцій) з деякими умовами виклику як бінарної межі, спільної з нативним (машинним рівнем ISA) кодом. Це було б явно дорогим, зокрема, при підтримці покажчика стека , який часто утримується безпосередньо в регістрі рівня ISA (з певними конкретними інструкціями для доступу до машини). Вказівник стеку вказує межу верхнього кадру виклику функції (в даний час активний). Коли вводиться виклик функції, потрібен новий кадр, а покажчик стека додається або віднімається (залежно від умовності ISA) на значення, яке не менше необхідного розміру кадру. Потім кадр, як кажуть, виділенийколи вказівник стека після операцій. Параметри функцій можуть передаватися і на кадр стека, залежно від умовності виклику, що використовується для виклику. Кадр може містити пам'ять автоматичних об'єктів (можливо, включаючи параметри), визначені вихідним кодом C ++. У сенсі таких реалізацій ці об’єкти "виділяються". Коли управління виходить з виклику функції, кадр більше не потрібен, він зазвичай звільняється шляхом відновлення покажчика стека назад у стан перед викликом (збереженим раніше згідно з умовами виклику). Це можна розглядати як "угоду". Ці операції роблять запис активації ефективно структурою даних LIFO, тому його часто називають " стеком (викликом) ".
Оскільки більшість реалізацій C ++ (особливо ті, які орієнтуються на рідний код рівня ISA та використовують мову складання як безпосередній вихід), використовують подібні стратегії, як ця, така заплутана схема "розподілу" є популярною. Такі асигнування (як і транслокації) проводять машинні цикли, і це може бути дорого, коли (неоптимізовані) дзвінки трапляються часто, хоча сучасні мікроархітектури процесора можуть мати складні оптимізації, реалізовані апаратно для загальної схеми коду (наприклад, використання стековий двигун у виконанні PUSH
/ POP
інструкціях).
Але в будь-якому випадку, в цілому, правда, що вартість розподілу кадру стека значно менша, ніж виклик функції розподілу, що керує безкоштовним магазином (якщо вона повністю не оптимізована) , яка сама може мати сотні (якщо не мільйони :-) операції по підтримці покажчика стека та інших станів. Функції розподілу зазвичай базуються на API, наданому розміщеним середовищем (наприклад, час виконання, передбачений ОС). Такі відмінності від призначення проведення автоматичних об'єктів для викликів функцій, такі розподіли мають загальний характер, тому вони не матимуть структуру кадру, як стек. Традиційно вони виділяють простір із сховища в басейні під назвою купи (або декілька купи). На відміну від "стека", поняття "купа" тут не вказує на структуру даних, що використовується; це отримано з ранньої мовної реалізації десятиліть тому. (BTW, стек викликів зазвичай виділяється з фіксованим або визначеним користувачем розміром з купи оточенням середовища при запуску програми або потоку.) Характер випадків використання ускладнює розподіл і розстановку з купи набагато складніше (ніж push або pop of кадри стека), і навряд чи можливо їх безпосередньо оптимізувати апаратним забезпеченням.
Вплив на доступ до пам'яті
Звичайний розподіл стеків завжди ставить новий кадр вгорі, тому він має досить непогану місцевість. Це дружньо до кеша. OTOH, пам'ять, виділена випадковим чином у вільному магазині, не має такого властивості. Оскільки ISO C ++ 17, існують шаблони ресурсів пулу, які надає компанія <memory>
. Пряме призначення такого інтерфейсу - дозволити, щоб результати послідовних виділень були близько в пам'яті. Це підтверджує той факт, що ця стратегія, як правило, хороша для роботи із сучасними реалізаціями, наприклад, дружніми для кешування в сучасних архітектурах. Хоча це стосується продуктивності доступу, а не розподілу .
Паралельність
Очікування одночасного доступу до пам’яті може мати різний вплив між стеком і купами. Стеку викликів, як правило, належить виключно один потік виконання у реалізації C ++. OTOH, купи часто поділяються між потоками в процесі. Для таких груп функції розподілу та розподілу повинні захищати спільну внутрішню адміністративну структуру даних від перегонів даних. Як результат, розподіли купи та розстановки можуть мати додаткові накладні витрати через внутрішні операції синхронізації.
Ефективність простору
Через характер випадків використання та внутрішніх структур даних, купи можуть страждати від фрагментації внутрішньої пам'яті , тоді як стек цього не відбувається. Це не має прямих впливів на продуктивність розподілу пам'яті, але в системі з віртуальною пам'яттю низька ефективність простору може знизити загальну ефективність доступу до пам’яті. Це особливо жахливо, коли HDD використовується як обмін фізичною пам'яттю. Це може спричинити досить тривалі затримки - іноді мільярди циклів.
Обмеження розподілу стеків
Незважаючи на те, що розподілення стеків часто перевершують продуктивність, ніж розподіли в купі реальності, це, звичайно, не означає, що розподіли стеків завжди можуть замінити купірування.
По-перше, немає можливості виділити простір у стеці з розміром, визначеним під час виконання, портативним способом з ISO C ++. Є розширення, що надаються подібними реалізаціямиalloca
VLA + G ++ (масив змінної довжини), але є причини, щоб їх уникнути. (Джерело IIRC, Linux недавно видаляє використання VLA.) (Також зауважте, що ISO C99 має мандат VLA, але ISO C11 не підтримує підтримку.)
По-друге, не існує надійного і портативного способу виявлення виснаження місця у стопі. Це часто називають переповненням стека (хм, етимологія цього сайту) , але, мабуть, точніше, переповнення стека . Насправді це часто спричиняє недійсний доступ до пам'яті, і стан програми потім пошкоджується (... а може, ще гірше, дірка безпеки). Насправді ISO C ++ не має поняття "стек" і робить його невизначеною поведінкою, коли ресурс вичерпується . Будьте обережні, скільки місця повинно залишитися для автоматичних об'єктів.
Якщо місця в стеці вичерпано, в стеці виділено занадто багато об'єктів, що може бути викликано занадто великою кількістю активних викликів функцій або неправильним використанням автоматичних об'єктів. Такі випадки можуть припускати наявність помилок, наприклад, рекурсивний виклик функції без правильних умов виходу.
Тим не менш, іноді бажані глибокі рекурсивні дзвінки. У реалізаціях мов, що потребують підтримки незв’язаних активних дзвінків (де глибина виклику обмежена лише загальною пам'яттю), неможливо використовувати (сучасний) власний стек виклику безпосередньо як запис активації цільової мови, як типовий C ++ реалізація. Щоб вирішити проблему, потрібні альтернативні способи побудови записів активації. Наприклад, SML / NJ явно виділяє кадри на купу і використовує стеки кактусів . Складне розподіл таких кадрів запису активації зазвичай не таке швидке, як кадри стека викликів. Однак якщо такі мови впроваджуються далі з гарантією правильної рекурсії хвоста, пряме виділення стека в мові об'єкта (тобто "об'єкт" у мові не зберігається як посилання, але нативні примітивні значення, які можуть бути відображені один на один, зіставлені на нерозподілені об'єкти C ++), ще складніше, ніж більше виконавчий штраф загалом. Використовуючи C ++ для реалізації таких мов, складно оцінити ефективність роботи.