У таких мовах, як C і C ++, при використанні покажчиків на змінні нам потрібно ще одне місце пам'яті для зберігання цієї адреси. Так це не пам’ять накладні? Як це компенсується? Чи використовуються покажчики у критично важливих для часу додатках?
У таких мовах, як C і C ++, при використанні покажчиків на змінні нам потрібно ще одне місце пам'яті для зберігання цієї адреси. Так це не пам’ять накладні? Як це компенсується? Чи використовуються покажчики у критично важливих для часу додатках?
Відповіді:
Насправді накладні витрати насправді не лежать у зайвих 4 або 8 байтах, необхідних для зберігання вказівника. У більшості разів покажчики використовуються для динамічного розподілу пам’яті , це означає, що ми викликаємо функцію для виділення блоку пам’яті, і ця функція повертає нам покажчик, який вказує на цей блок пам’яті. Цей новий блок сам по собі представляє значні витрати.
Тепер вам не потрібно займатися розподілом пам'яті, щоб використовувати вказівник: у вас може бути масив int
оголошених статично або на стеці, а ви можете використовувати вказівник замість індексу для відвідування int
s, і це все дуже приємно, просто і ефективно. Не потрібно виділяти пам'ять, і покажчик зазвичай займає стільки ж місця в пам'яті, як і цілий індекс.
Крім того, як нагадує нам Джошуа Тейлор у коментарі, вказівники використовуються для передачі чогось посилання. Наприклад, struct foo f; init_foo(&f);
виділить f на стек, а потім дзвонить init_foo()
за допомогою вказівника на це struct
. Це дуже часто. (Будьте обережні, щоб не передати ці покажчики "вгору".) У C ++ ви можете бачити, що це робиться з "посиланням" ( foo&
) замість вказівника, але посилання є не що інше, як покажчики, які ви можете не змінювати, і вони займають стільки ж пам’яті.
Але основна причина, по якій використовуються покажчики, - це динамічне розподілення пам’яті, і це робиться для того, щоб вирішити проблеми, які неможливо було вирішити інакше. Ось спрощений приклад: Уявіть, що ви хочете прочитати весь вміст файлу. Де ви збираєтесь їх зберігати? Якщо ви спробуєте використовувати буфер фіксованого розміру, тоді ви зможете читати лише файли, які не перевищують цей буфер. Але, використовуючи розподіл пам'яті, ви можете виділити стільки пам'яті, скільки потрібно, щоб прочитати файл, а потім перейти до його читання.
Крім того, C ++ є об'єктно-орієнтованою мовою, і є певні аспекти OOP, такі як абстракція , які досяжні лише за допомогою покажчиків. Навіть такі мови, як Java та C #, широко використовують покажчики, вони просто не дозволяють вам безпосередньо маніпулювати покажчиками, щоб запобігти вам робити небезпечні речі з ними, але все-таки ці мови починають мати сенс лише після того, як у вас є зрозумів, що за лаштунками все робиться за допомогою покажчиків.
Отже, покажчики використовуються не лише у критичних за часом програмах з низькою пам'яттю, вони використовуються всюди .
struct foo f; init_foo(&f);
Виділить f
на стеку, а потім викликають init_foo
вказівником на цю структуру. Це дуже часто. (Будьте обережні, щоб не вказати ці покажчики "вгору".)
malloc
мають ДУЖЕ НИСКО заголовок, оскільки вони кластерують виділені блоки у "відрах". З іншого боку, це взагалі означає перерозподіл: ви вимагаєте 35 байт і отримуєте 64 (без вашого відома), таким чином витрачаючи 29 ...
Так це не пам’ять накладні?
Звичайно, додаткова адреса (як правило, 4/8 байт залежно від процесора).
Як це компенсується?
Це не так. Якщо вам потрібна опосередкована необхідна для покажчиків, то ви отримаєте оплату за неї.
Чи використовуються покажчики у критично важливих для часу додатках?
Я там мало пропрацював, але я би припустив, що так. Доступ до вказівника є елементарним аспектом програмування монтажу. Це займає тривіальні обсяги пам’яті, а операції з вказівниками є швидкими - навіть у контексті подібних програм.
У мене не так само, як у Теластині.
Системні глобалі у вбудованому процесорі можуть бути адресовані конкретними, жорстко кодованими адресами.
Глобали в програмі будуть розглядатися як зміщення від спеціального покажчика, який вказує на місце в пам'яті, де зберігаються глобалі та статика.
Локальні змінні з'являються при введенні функції і адресовані як зміщення від іншого спеціального покажчика, який часто називають "покажчиком кадру". Сюди входять аргументи функції. Якщо ви обережно ставитеся до натискань та спливаючих елементів із вказівником стека, ви можете усувати вказівник кадру та отримати доступ до локальних змінних прямо із вказівника стека.
Таким чином, ви платите за опосередкування покажчиків, чи рухаєтесь ви по масиву, чи просто захоплюєте якусь не примітну локальну чи глобальну змінну. Він просто ґрунтується на іншому вказівнику, залежно від того, яка це змінна зміна. Код, добре складений, буде зберігати цей покажчик у регістрі процесора, а не перезавантажувати його щоразу, коли він використовується.
Так, звісно. Але це врівноважуючий акт.
Програми з низькою пам’яттю, як правило, створюються з урахуванням компромісу між накладними витратами на кілька змінних вказівників порівняно з накладними витратами на масову програму (яка повинна зберігатися в пам’яті, пам’ятайте!), Якщо покажчики не вдалося використовувати .
Цей розгляд стосується всіх програм, тому що ніхто не хоче будувати жахливий, незбагненний безлад з дублюваним кодом зліва та справа, що в двадцять разів більший, ніж це має бути.
У таких мовах, як C і C ++, при використанні покажчиків на змінні нам потрібно ще одне місце пам'яті для зберігання цієї адреси. Так це не пам’ять накладні?
Ви припускаєте, що вказівник потрібно зберігати. Це не завжди так. Кожна змінна зберігається за деякою адресою пам'яті. Скажіть, у вас long
оголошено як long n = 5L;
. Це виділяє сховище за n
деякою адресою. Ми можемо використовувати цю адресу, щоб робити химерні речі, як *((char *) &n) = (char) 0xFF;
маніпулювати частинами n
. Адреса n
ніде не зберігається як додаткові накладні витрати.
Як це компенсується?
Навіть якщо вказівники явно зберігаються (наприклад, у структурах даних, таких як списки), отримана структура даних часто є більш елегантною (простішою, легшою для розуміння, простішою в обробці тощо), ніж еквівалентна структура даних без покажчиків.
Чи використовуються покажчики у критично важливих для часу додатках?
Так. Пристрої, що використовують мікроконтролери, часто містять дуже мало пам’яті, але прошивка може використовувати вказівники для обробки векторів переривання або управління буфером тощо.
gcc -fverbose-asm -S -O2
зібрати якийсь код C)
Наявність вказівника, безумовно, вимагає певних накладних витрат, але ви також можете бачити перевершення. Вказівник як індекс. У C ви можете використовувати складні структури даних, такі як рядок і структури, лише за допомогою покажчиків.
Насправді, припустимо, ви хочете передати змінну за посиланням, тоді її легко підтримувати вказівник, а не тиражувати всю структуру та синхронізувати зміни між ними (навіть для їх копіювання вам знадобиться вказівник). Як би ви попрацювали з безперервним розподілом пам’яті та розмежуванням без покажчика?
Навіть ваші звичайні змінні містять запис у таблиці символів, у якій зберігається адреса, куди вказана ваша змінна. Отже, я не думаю, що це створює великі накладні витрати з точки зору пам'яті (всього 4 або 8 байт). Навіть такі мови, як java, використовують покажчики всередині (довідкові), вони просто не дозволяють вам маніпулювати ними, оскільки це зробить JVM менш захищеним.
Ви повинні використовувати вказівники лише тоді, коли у вас немає іншого вибору, як, наприклад, відсутні типи даних, структури (в), оскільки використання покажчиків може призвести до помилок, якщо їх не обробляти належним чином і порівняно складніше їх налагоджувати.
Так це не пам’ять накладні?
Так .... ні ... можливо?
Це незручне запитання, оскільки уявіть собі діапазон адрес пам’яті на апараті та програмне забезпечення, яке потрібно наполегливо відслідковувати, де знаходяться речі в пам’яті, таким чином, що їх не можна прив’язати до стека.
Наприклад, уявіть музичний плеєр, у якому музичний файл завантажується користувачем із натискання кнопки та вивантажується з летючої пам'яті, коли користувач намагається завантажити інший музичний файл.
Як ми відстежуємо, де зберігаються аудіодані? Нам потрібна адреса пам'яті до нього. Програмі потрібно не тільки відслідковувати фрагменти аудіоданих у пам’яті, але й там, де вона знаходиться в пам’яті. Таким чином нам потрібно тримати навколо пам'яті адресу (тобто вказівник). А розмір пам'яті, необхідний для адреси пам'яті, буде відповідати діапазону адресації машини (наприклад: 64-бітний покажчик для 64-бітного діапазону адресації).
Таким чином, це "так", воно вимагає зберігання, щоб відслідковувати адресу пам'яті, але це не так, як ми можемо уникати цього для динамічно розподіленої пам'яті такого типу.
Як це компенсується?
Якщо говорити лише про розмір самого вказівника, ви можете уникнути витрат у деяких випадках, використовуючи стек, наприклад, у цьому випадку компілятори можуть генерувати інструкції, які ефективно жорстко кодують відносну адресу пам'яті, уникаючи вартості вказівника. Тим не менш, це залишає вас вразливими до переповнення стека, якщо ви робите це для великих розмірів з змінним розміром, а також, як правило, непрактично (якщо не прямо неможливо) зробити для складної серії гілок, керованих введенням користувача (як у аудіо-прикладі вище).
Інший спосіб - використовувати більш суміжні структури даних. Наприклад, послідовність на основі масиву може використовуватися замість подвійно пов'язаного списку, на який потрібні два покажчики на вузол. Ми також можемо використовувати гібрид цих двох, як нерозгорнутий список, який зберігає лише покажчики між кожною суміжною групою з N елементів.
Чи використовуються покажчики у критично важливих для часу додатках?
Так, дуже часто, оскільки багато критично важливих для роботи програм написані на C або C ++, в яких переважає використання вказівника (вони можуть бути за розумним вказівником або контейнером на зразок std::vector
або std::string
, але основна механіка зводиться до покажчика, який використовується відстежувати адресу до динамічного блоку пам'яті).
Тепер повернемось до цього питання:
Як це компенсується? (Частина друга)
Покажчики, як правило, дешеві від забруднень, якщо ви не зберігаєте їх як мільйон (що все-таки є неміцним * 8 мегабайт на 64-бітній машині).
* Зауважте, як Бен вказував, що "потворний" 8 мег все ще має розмір кешу L3. Тут я використав "потворне" більше в сенсі загального використання DRAM, і типовий відносний розмір до фрагментів пам'яті, на яке вказуватиме здорове використання покажчиків.
Там, де вказівники дорожчають, це не самі вказівники, а:
Динамічне розподіл пам'яті. Динамічне розподілення пам'яті, як правило, дороге, оскільки воно має проходити через базову структуру даних (наприклад: приятель або розподільник платів). Незважаючи на те, що вони часто оптимізовані до загибелі, вони загального призначення і призначені для обробки блоків змінного розміру, які вимагають зробити хоч трохи роботи, що нагадує "пошук" (хоч і легкий і, можливо, навіть постійний час) для знайти вільний набір суміжних сторінок в пам'яті.
Доступ до пам'яті. Це, як правило, більша накладні витрати, щоб турбуватися. Щоразу, коли ми отримуємо доступ до пам’яті, виділеної динамічно, вперше, виникає обов'язкова помилка сторінки, а також кеш-помилки переміщують пам’ять вниз по ієрархії пам’яті та вниз в регістр.
Доступ до пам'яті
Доступ до пам'яті - один з найважливіших аспектів продуктивності, крім алгоритмів. Багато критично важливих для продуктивності галузей, таких як ігрові двигуни AAA, спрямовують велику частину своєї енергії на оптимізацію, орієнтовану на дані, яка зводиться до більш ефективних моделей доступу до пам’яті.
Однією з найбільших труднощів у роботі мов вищого рівня, які хочуть виділити кожен визначений користувачем тип окремо за допомогою сміттєзбірника, наприклад, є те, що вони можуть трохи фрагментувати пам'ять. Це може бути особливо вірно, якщо не всі об’єкти виділені відразу.
У тих випадках, якщо ви зберігаєте список мільйона екземплярів визначеного користувачем типу об’єкта, доступ до цих примірників послідовно в циклі може бути досить повільним, оскільки це аналогічно списку мільйона покажчиків, які вказують на розрізнені області пам'яті. У таких випадках архітектура хоче отримати форму пам’яті у верхньому, повільнішому та більшому рівнях ієрархії у великих, вирівняних фрагментах з надією, що навколишні дані в цих фрагментах будуть доступні до виселення. Коли кожен об’єкт у такому списку виділяється окремо, то часто ми закінчуємо оплату за нього з помилками кешу, коли кожна наступна ітерація може мати завантаження з абсолютно іншої області пам’яті, без сусідніх об’єктів, до яких не можна отримати доступ до виселення.
Багато компіляторів для таких мов роблять справді чудову роботу в даний час підбором інструкцій та розподілом реєстру, але відсутність більш прямого контролю над управлінням пам’яттю тут може бути вбивчою (хоча часто менш схильною до помилок) і все-таки зробити такі мови, як C і C ++ досить популярні.
Непряма оптимізація доступу вказівника
У найбільш критичних для роботи сценаріях програми часто використовують пули пам’яті, які об’єднують пам’ять з суміжних фрагментів, щоб поліпшити локальність відліку. У таких випадках навіть пов'язана структура, як дерево або зв'язаний список, може бути кешованою, якщо макет пам'яті її вузлів є суміжним за своєю природою. Це фактично робить деренференціювання покажчиків дешевшою, хоча і опосередкованою, покращуючи локальність відліку, яка бере участь у їх перенаправлення.
Переслідування вказівників навколо
Припустимо, у нас є окремо пов'язаний список на зразок:
Foo->Bar->Baz->null
Проблема полягає в тому, що якщо ми виділимо всі ці вузли окремо проти розподільника загального призначення (і, можливо, не всі одразу), фактична пам'ять може бути розсіяна дещо так (спрощена схема):
Коли ми починаємо переслідувати вказівники навколо та Foo
отримуємо доступ до вузла, ми починаємо з обов'язкового пропуску (і, можливо, помилки сторінки), переміщуючи шматок із своєї області пам’яті з повільніших регіонів пам’яті до більш швидких областей пам’яті, наприклад:
Це змушує нас кешувати (можливо, також сторінку) області пам’яті лише для доступу до її частини та виселяти решту, коли ми переслідуємо покажчики навколо цього списку. Однак, взявши під контроль розподільник пам'яті, ми можемо безперервно виділити такий список:
... і тим самим значно покращити швидкість, з якою ми можемо знецінити ці покажчики та обробити їх пуанси. Отже, хоч і дуже опосередковано, ми можемо прискорити доступ до цього вказівника. Звичайно, якби ми просто зберігали їх у безперервному масиві, ми б не мали цього питання в першу чергу, але розподільник пам'яті, який дає нам явний контроль над компонуванням пам'яті, може врятувати день, коли потрібна пов'язана структура.
* Примітка: це дуже спрощена діаграма та дискусія про ієрархію пам’яті та місцеположення, але, сподіваємось, це підходить для рівня питання.
Так це не пам’ять накладні?
Це справді пам’ять, але дуже маленька (до незначності).
Як це компенсується?
Це не компенсується. Вам потрібно усвідомити, що доступ до даних за допомогою вказівника (перенаправлення покажчика) надзвичайно швидкий (якщо я правильно пам’ятаю, він використовує лише одну інструкцію по збірці на кожну дереференцію). Це досить швидко, що це буде у багатьох випадках найшвидшою альтернативою у вас.
Чи використовуються покажчики у критично важливих для часу додатках?
Так.
Потрібно лише додаткове використання пам'яті (як правило, 4-8 байт на покажчик), тоді як вам потрібен цей вказівник. Існує багато методик, які роблять це більш доступним.
Найбільш фундаментальна методика, яка робить покажчики потужними, - це не потрібно тримати кожен покажчик. Іноді ви можете використовувати алгоритм для побудови покажчика з вказівника на щось інше. Найбільш тривіальний приклад цього - арифметика масиву. Якщо ви виділите масив з 50 цілих чисел, вам не потрібно зберігати 50 покажчиків, по одному на кожне ціле число. Зазвичай ви відстежуєте один вказівник (перший) і використовуєте арифметику вказівника, щоб генерувати інші на льоту. Іноді ви можете тимчасово зберігати один із цих покажчиків на певний елемент масиву, але лише тоді, коли вам це потрібно. Щойно ви закінчите, ви можете його відкинути, якщо ви зберегли достатньо інформації, щоб згодом відновити її, якщо вона вам потрібна. Це може здатися тривіальним, але саме такі інструменти збереження ви '
У надзвичайно жорстких ситуаціях з пам'яттю це можна використовувати для мінімізації витрат. Якщо ви працюєте в дуже тісному просторі пам'яті, у вас зазвичай добре розуміється, скільки об’єктів потрібно маніпулювати. Замість того, щоб виділяти купу цілих чисел по одному та зберігати на них повні покажчики, ви можете скористатися своїми знаннями розробника про те, що у цьому конкретному алгоритмі ви ніколи не матимете більше 256 цілих чисел. У цьому випадку ви можете зберегти вказівник на перше ціле число і відслідковувати індекс за допомогою знака char (1 байт), а не з використанням повного вказівника (4/8 байт). Ви також можете використовувати алгоритмічні трюки, щоб генерувати деякі з цих показників на ходу.
Така свідомість пам'яті була дуже популярною в минулому. Наприклад, ігри NES значною мірою покладаються на їхню здатність копіювати дані та генерувати покажчики алгоритмічно, а не потрібно зберігати їх усі оптом.
Екстремальні ситуації з пам’яттю також можуть призвести до таких дій, як виділення всіх просторів, над якими ви працюєте під час компіляції. Тоді вказівник, який потрібно зберегти на цю пам'ять, зберігається в програмі, а не в даних. У багатьох ситуаціях, що обмежуються пам'яттю, у вас є окрема пам'ять програми та даних (часто ROM та RAM), тому ви, можливо, зможете налаштувати спосіб використання алгоритму для просування покажчиків у пам'ять програми.
По суті, ви не зможете позбутися від усіх накладних витрат. Однак ви можете це контролювати. Використовуючи алгоритмічні методи, ви можете мінімізувати кількість покажчиків, які ви можете зберігати. Якщо ви, звичайно, використовуєте покажчики динамічної пам'яті, ви ніколи не будете меншими, ніж утримувати 1 покажчик на це місце динамічної пам’яті, оскільки це мінімально мінімальна кількість інформації, необхідна для доступу до чого-небудь у цьому блоці пам'яті. Однак у надзвичайно жорстких сценаріях обмеження пам’яті це, як правило, особливий випадок (динамічна пам’ять та надзвичайно жорсткі обмеження пам’яті, як правило, не з’являються в одних і тих же ситуаціях).
У багатьох ситуаціях покажчики фактично економлять пам'ять. Поширена альтернатива використанню покажчиків - це зробити копію структури даних. Повна копія структури даних буде більшою, ніж покажчик.
Одним із прикладів критично важливих для часу додатків є мережевий стек. Хороший мережевий стек буде розроблений як "нульова копія" - і для цього потрібне розумне використання покажчиків.