Хіба використання змінних вказівника не є головною пам'яттю?


29

У таких мовах, як C і C ++, при використанні покажчиків на змінні нам потрібно ще одне місце пам'яті для зберігання цієї адреси. Так це не пам’ять накладні? Як це компенсується? Чи використовуються покажчики у критично важливих для часу додатках?


11
Переваги динамічного розподілу пам’яті значно перевищують вартість покажчика.
Джеймс Маклеод

32
Як ви думаєте, як інші мови (Java, C #, ...) зберігають посилання на об'єкти? (Підказка: вони використовують вказівники).
Ерік Ейдт

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

9
Як щодо передачі (великого) структурного аргументу за адресою? Якщо ви вважаєте, що це змінна вказівник, вона неминуча для багатьох алгоритмів і використовує набагато менше місця, ніж передача структури за значенням!
PJTraill

4
Це відчуває деякі домашні завдання. Питання призначене для вивчення того, чи відповідь розуміє покажчики та як вони використовуються.
Майкл Шоу

Відповіді:


34

Насправді накладні витрати насправді не лежать у зайвих 4 або 8 байтах, необхідних для зберігання вказівника. У більшості разів покажчики використовуються для динамічного розподілу пам’яті , це означає, що ми викликаємо функцію для виділення блоку пам’яті, і ця функція повертає нам покажчик, який вказує на цей блок пам’яті. Цей новий блок сам по собі представляє значні витрати.

Тепер вам не потрібно займатися розподілом пам'яті, щоб використовувати вказівник: у вас може бути масив intоголошених статично або на стеці, а ви можете використовувати вказівник замість індексу для відвідування ints, і це все дуже приємно, просто і ефективно. Не потрібно виділяти пам'ять, і покажчик зазвичай займає стільки ж місця в пам'яті, як і цілий індекс.

Крім того, як нагадує нам Джошуа Тейлор у коментарі, вказівники використовуються для передачі чогось посилання. Наприклад, struct foo f; init_foo(&f);виділить f на стек, а потім дзвонить init_foo()за допомогою вказівника на це struct. Це дуже часто. (Будьте обережні, щоб не передати ці покажчики "вгору".) У C ++ ви можете бачити, що це робиться з "посиланням" ( foo&) замість вказівника, але посилання є не що інше, як покажчики, які ви можете не змінювати, і вони займають стільки ж пам’яті.

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

Крім того, C ++ є об'єктно-орієнтованою мовою, і є певні аспекти OOP, такі як абстракція , які досяжні лише за допомогою покажчиків. Навіть такі мови, як Java та C #, широко використовують покажчики, вони просто не дозволяють вам безпосередньо маніпулювати покажчиками, щоб запобігти вам робити небезпечні речі з ними, але все-таки ці мови починають мати сенс лише після того, як у вас є зрозумів, що за лаштунками все робиться за допомогою покажчиків.

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


5
"основна причина, по якій використовуються покажчики, - це динамічне розподілення пам'яті". Це може бути в C ++, але не обов'язково в C. У C покажчики - ваш єдиний спосіб передавати щось за посиланням. Якщо ви не хочете копіювати всю структуру, вам знадобиться вказівник на неї. Це все ще має сенс, навіть якщо ви взагалі не робите динамічного розподілу. Напр., struct foo f; init_foo(&f);Виділить fна стеку, а потім викликають init_fooвказівником на цю структуру. Це дуже часто. (Будьте обережні, щоб не вказати ці покажчики "вгору".)
Джошуа Тейлор

@JoshuaTaylor це дуже правильно, я забув про це. Чи можу я внести зміни до своєї відповіді? (Це вважається хорошою практикою для програмістів SE, оскільки коментарі ефемерні, а відповіді - ні.)
Майк Накіс

Неодмінно додайте на це свою відповідь. :)
Джошуа Тейлор

2
Це являє собою значні накладні витрати самі по собі, оскільки цей блок матиме (прихований) заголовок, який зазвичай буде розміром як кілька покажчиків. => Насправді, сучасні реалізації mallocмають ДУЖЕ НИСКО заголовок, оскільки вони кластерують виділені блоки у "відрах". З іншого боку, це взагалі означає перерозподіл: ви вимагаєте 35 байт і отримуєте 64 (без вашого відома), таким чином витрачаючи 29 ...
Матьє М.

1
@MikeNakis: Виглядає краще, спасибі за те, що тримаєтесь зі мною :)
Matthieu M.

35

Так це не пам’ять накладні?

Звичайно, додаткова адреса (як правило, 4/8 байт залежно від процесора).

Як це компенсується?

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

Чи використовуються покажчики у критично важливих для часу додатках?

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


1
@DavidGrinberg Це лише припускаючи, що немає оптимізації зворотного значення .
Darkhogg

3
Я використовував покажчики, коли писав програми TSR для DOS, які повинні були поміститися в 15 к ще у вісімдесятих. Так так, вони використовуються в додатках із низькою пам'яттю.
Гурт Робота

1
@StevenBurnap Ви називаєте 15 кб низької пам’яті для програми TSR? Я колись написав інструмент TSR, який спожив лише 16 байт пам'яті.
kasperd

22
@kasperd: І в моєму дні у нас були лише нулі
Джек


11

У мене не так само, як у Теластині.

Системні глобалі у вбудованому процесорі можуть бути адресовані конкретними, жорстко кодованими адресами.

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

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

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


6

Так, звісно. Але це врівноважуючий акт.

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

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


5

У таких мовах, як C і C ++, при використанні покажчиків на змінні нам потрібно ще одне місце пам'яті для зберігання цієї адреси. Так це не пам’ять накладні?

Ви припускаєте, що вказівник потрібно зберігати. Це не завжди так. Кожна змінна зберігається за деякою адресою пам'яті. Скажіть, у вас longоголошено як long n = 5L;. Це виділяє сховище за nдеякою адресою. Ми можемо використовувати цю адресу, щоб робити химерні речі, як *((char *) &n) = (char) 0xFF;маніпулювати частинами n. Адреса nніде не зберігається як додаткові накладні витрати.

Як це компенсується?

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

Чи використовуються покажчики у критично важливих для часу додатках?

Так. Пристрої, що використовують мікроконтролери, часто містять дуже мало пам’яті, але прошивка може використовувати вказівники для обробки векторів переривання або управління буфером тощо.


Деякі змінні не зберігаються в пам'яті, а лише в регістрах
Базиль Старинкевич

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

Компілятору може бути дозволено зберігати деякі локальні змінні лише в регістрах, і більшість оптимізуючих компіляторів роблять це (спробуйте gcc -fverbose-asm -S -O2зібрати якийсь код C)
Basile Starynkevitch

@BasileStarynkevitch Я не впевнений у точці, яку ви намагаєтеся зробити, зауваживши, що змінні можуть зберігатися виключно в регістрах. Чи можете ви, будь ласка, докладно?
Лоуренс

5

Наявність вказівника, безумовно, вимагає певних накладних витрат, але ви також можете бачити перевершення. Вказівник як індекс. У C ви можете використовувати складні структури даних, такі як рядок і структури, лише за допомогою покажчиків.

Насправді, припустимо, ви хочете передати змінну за посиланням, тоді її легко підтримувати вказівник, а не тиражувати всю структуру та синхронізувати зміни між ними (навіть для їх копіювання вам знадобиться вказівник). Як би ви попрацювали з безперервним розподілом пам’яті та розмежуванням без покажчика?

Навіть ваші звичайні змінні містять запис у таблиці символів, у якій зберігається адреса, куди вказана ваша змінна. Отже, я не думаю, що це створює великі накладні витрати з точки зору пам'яті (всього 4 або 8 байт). Навіть такі мови, як java, використовують покажчики всередині (довідкові), вони просто не дозволяють вам маніпулювати ними, оскільки це зробить JVM менш захищеним.

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


3

Так це не пам’ять накладні?

Так .... ні ... можливо?

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

Наприклад, уявіть музичний плеєр, у якому музичний файл завантажується користувачем із натискання кнопки та вивантажується з летючої пам'яті, коли користувач намагається завантажити інший музичний файл.

Як ми відстежуємо, де зберігаються аудіодані? Нам потрібна адреса пам'яті до нього. Програмі потрібно не тільки відслідковувати фрагменти аудіоданих у пам’яті, але й там, де вона знаходиться в пам’яті. Таким чином нам потрібно тримати навколо пам'яті адресу (тобто вказівник). А розмір пам'яті, необхідний для адреси пам'яті, буде відповідати діапазону адресації машини (наприклад: 64-бітний покажчик для 64-бітного діапазону адресації).

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

Як це компенсується?

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

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

Чи використовуються покажчики у критично важливих для часу додатках?

Так, дуже часто, оскільки багато критично важливих для роботи програм написані на C або C ++, в яких переважає використання вказівника (вони можуть бути за розумним вказівником або контейнером на зразок std::vectorабо std::string, але основна механіка зводиться до покажчика, який використовується відстежувати адресу до динамічного блоку пам'яті).

Тепер повернемось до цього питання:

Як це компенсується? (Частина друга)

Покажчики, як правило, дешеві від забруднень, якщо ви не зберігаєте їх як мільйон (що все-таки є неміцним * 8 мегабайт на 64-бітній машині).

* Зауважте, як Бен вказував, що "потворний" 8 мег все ще має розмір кешу L3. Тут я використав "потворне" більше в сенсі загального використання DRAM, і типовий відносний розмір до фрагментів пам'яті, на яке вказуватиме здорове використання покажчиків.

Там, де вказівники дорожчають, це не самі вказівники, а:

  1. Динамічне розподіл пам'яті. Динамічне розподілення пам'яті, як правило, дороге, оскільки воно має проходити через базову структуру даних (наприклад: приятель або розподільник платів). Незважаючи на те, що вони часто оптимізовані до загибелі, вони загального призначення і призначені для обробки блоків змінного розміру, які вимагають зробити хоч трохи роботи, що нагадує "пошук" (хоч і легкий і, можливо, навіть постійний час) для знайти вільний набір суміжних сторінок в пам'яті.

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

Доступ до пам'яті

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

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

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

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

Непряма оптимізація доступу вказівника

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

Переслідування вказівників навколо

Припустимо, у нас є окремо пов'язаний список на зразок:

Foo->Bar->Baz->null

Проблема полягає в тому, що якщо ми виділимо всі ці вузли окремо проти розподільника загального призначення (і, можливо, не всі одразу), фактична пам'ять може бути розсіяна дещо так (спрощена схема):

введіть тут опис зображення

Коли ми починаємо переслідувати вказівники навколо та Fooотримуємо доступ до вузла, ми починаємо з обов'язкового пропуску (і, можливо, помилки сторінки), переміщуючи шматок із своєї області пам’яті з повільніших регіонів пам’яті до більш швидких областей пам’яті, наприклад:

введіть тут опис зображення

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

введіть тут опис зображення

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

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


1
"неміцний 8 мегабайт" - типовий розмір усього кешу L3 на сучасному процесорі
Бен Войгт

1
@BenVoigt Я спробую це трохи відключити. Звичайно, це було б жахливо, якби кожен вказівник вказував на 32-бітний шматок пам'яті, наприклад

@BenVoigt Додав виноску, щоб спробувати уточнити цю частину!

Це уточнення виглядає добре.
Бен Войгт

1

Так це не пам’ять накладні?

Це справді пам’ять, але дуже маленька (до незначності).

Як це компенсується?

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

Чи використовуються покажчики у критично важливих для часу додатках?

Так.


0

Потрібно лише додаткове використання пам'яті (як правило, 4-8 байт на покажчик), тоді як вам потрібен цей вказівник. Існує багато методик, які роблять це більш доступним.

Найбільш фундаментальна методика, яка робить покажчики потужними, - це не потрібно тримати кожен покажчик. Іноді ви можете використовувати алгоритм для побудови покажчика з вказівника на щось інше. Найбільш тривіальний приклад цього - арифметика масиву. Якщо ви виділите масив з 50 цілих чисел, вам не потрібно зберігати 50 покажчиків, по одному на кожне ціле число. Зазвичай ви відстежуєте один вказівник (перший) і використовуєте арифметику вказівника, щоб генерувати інші на льоту. Іноді ви можете тимчасово зберігати один із цих покажчиків на певний елемент масиву, але лише тоді, коли вам це потрібно. Щойно ви закінчите, ви можете його відкинути, якщо ви зберегли достатньо інформації, щоб згодом відновити її, якщо вона вам потрібна. Це може здатися тривіальним, але саме такі інструменти збереження ви '

У надзвичайно жорстких ситуаціях з пам'яттю це можна використовувати для мінімізації витрат. Якщо ви працюєте в дуже тісному просторі пам'яті, у вас зазвичай добре розуміється, скільки об’єктів потрібно маніпулювати. Замість того, щоб виділяти купу цілих чисел по одному та зберігати на них повні покажчики, ви можете скористатися своїми знаннями розробника про те, що у цьому конкретному алгоритмі ви ніколи не матимете більше 256 цілих чисел. У цьому випадку ви можете зберегти вказівник на перше ціле число і відслідковувати індекс за допомогою знака char (1 байт), а не з використанням повного вказівника (4/8 байт). Ви також можете використовувати алгоритмічні трюки, щоб генерувати деякі з цих показників на ходу.

Така свідомість пам'яті була дуже популярною в минулому. Наприклад, ігри NES значною мірою покладаються на їхню здатність копіювати дані та генерувати покажчики алгоритмічно, а не потрібно зберігати їх усі оптом.

Екстремальні ситуації з пам’яттю також можуть призвести до таких дій, як виділення всіх просторів, над якими ви працюєте під час компіляції. Тоді вказівник, який потрібно зберегти на цю пам'ять, зберігається в програмі, а не в даних. У багатьох ситуаціях, що обмежуються пам'яттю, у вас є окрема пам'ять програми та даних (часто ROM та RAM), тому ви, можливо, зможете налаштувати спосіб використання алгоритму для просування покажчиків у пам'ять програми.

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


0

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

Одним із прикладів критично важливих для часу додатків є мережевий стек. Хороший мережевий стек буде розроблений як "нульова копія" - і для цього потрібне розумне використання покажчиків.

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