Так разве это не накладные расходы памяти?
Да нет наверное?
Это неловкий вопрос, потому что представьте диапазон адресации памяти на машине и программное обеспечение, которое должно постоянно отслеживать, где что находится в памяти, таким образом, чтобы его нельзя было привязать к стеку.
Например, представьте музыкальный проигрыватель, в котором музыкальный файл загружается пользователем нажатием кнопки и выгружается из энергозависимой памяти, когда пользователь пытается загрузить другой музыкальный файл.
Как мы отслеживаем, где хранятся аудиоданные? Нам нужен адрес памяти для этого. Программа не только должна отслеживать аудио порции данных в памяти , но и где она находится в памяти. Таким образом, нам нужно хранить адрес памяти (т. Е. Указатель). И размер хранилища, требуемый для адреса памяти, будет соответствовать диапазону адресов компьютера (например: 64-разрядный указатель для 64-разрядного диапазона адресов).
Так что это вроде «да», для хранения адреса памяти требуется хранилище, но мы не можем избежать его для динамически выделяемой памяти такого рода.
Как это компенсируется?
Говоря только о размере самого указателя, вы можете избежать затрат в некоторых случаях, используя стек, например, в этом случае компиляторы могут генерировать инструкции, которые эффективно жестко кодируют относительный адрес памяти, избегая затрат указателя. Тем не менее, это делает вас уязвимым для переполнения стека, если вы делаете это для больших распределений с переменным размером, а также имеет тенденцию быть непрактичным (если не полностью невозможным) для сложной серии ветвей, управляемых пользовательским вводом (как в примере с аудио). над).
Другой способ - использовать более смежные структуры данных. Например, вместо двусвязного списка может использоваться последовательность на основе массива, для которой требуется два указателя на узел. Мы также можем использовать гибрид этих двух, как развернутый список, в котором хранятся только указатели между каждой смежной группой из N элементов.
Используются ли указатели в приложениях с нехваткой памяти?
Да, очень часто, так как многие критичные к производительности приложения написаны на C или C ++, в которых преобладает использование указателя (они могут находиться за интеллектуальным указателем или контейнером, подобным std::vector
или std::string
, но основная механика сводится к используемому указателю). отслеживать адрес к динамическому блоку памяти).
Теперь вернемся к этому вопросу:
Как это компенсируется? (Часть вторая)
Указатели, как правило, очень дешевы, если только вы не храните их как миллион (что все равно составляет всего 8 мегабайт на 64-битной машине).
* Обратите внимание, как Бен указал, что «жалкие» 8 мегабайт по-прежнему размер кеша L3. Здесь я использовал «жалкое» больше в смысле общего использования DRAM и типичного относительного размера блоков памяти, на которые будет указывать правильное использование указателей.
Где указатели становятся дорогими, это не сами указатели, а:
Динамическое распределение памяти. Динамическое выделение памяти имеет тенденцию быть дорогим, поскольку оно должно проходить через базовую структуру данных (например, распределитель контактов или slab). Несмотря на то, что они часто оптимизируются до смерти, они универсальны и предназначены для работы с блоками переменного размера, которые требуют, чтобы они выполняли хотя бы небольшую работу, напоминающую «поиск» (пусть и легкий и, возможно, даже постоянное время) для найти бесплатный набор смежных страниц в памяти.
Доступ к памяти. Это, как правило, больше заботы. Всякий раз, когда мы обращаемся к памяти, выделенной динамически в первый раз, возникает принудительная ошибка страницы, а также отсутствует пропуск кеша при перемещении памяти вниз по иерархии памяти и вниз в регистр.
Доступ к памяти
Доступ к памяти является одним из наиболее важных аспектов производительности помимо алгоритмов. Многие области, критически важные для производительности, такие как игровые движки AAA, направляют большую часть своей энергии на оптимизацию, ориентированную на данные, которая сводится к более эффективным схемам и схемам доступа к памяти.
Одна из самых больших проблем с производительностью языков высокого уровня, которые хотят выделять каждый определенный пользователем тип отдельно через сборщик мусора, например, состоит в том, что они могут немного фрагментировать память. Это может быть особенно верно, если не все объекты размещены одновременно.
В этих случаях, если вы храните список из миллиона экземпляров определенного пользователем типа объекта, последовательный доступ к этим экземплярам в цикле может быть довольно медленным, поскольку он аналогичен списку из миллиона указателей, которые указывают на разные области памяти. В этих случаях архитектуре требуется извлекать память из верхних, более медленных и больших уровней иерархии в больших выровненных чанках с надеждой на то, что окружающие данные в этих чанках будут доступны до выселения. Когда каждый объект в таком списке размещается отдельно, часто мы в конечном итоге платим за него из-за промахов в кеше, когда каждая последующая итерация может загружаться из совершенно другой области памяти без доступа к соседним объектам до выселения.
Многие компиляторы для таких языков в наши дни делают действительно большую работу по выбору инструкций и распределению регистров, но отсутствие более непосредственного контроля над управлением памятью здесь может быть убийственным (хотя часто менее подверженным ошибкам) и по-прежнему делать такие языки, как C и C ++ довольно популярны.
Косвенная оптимизация доступа к указателю
В наиболее критичных для производительности сценариях приложения часто используют пулы памяти, которые объединяют память из смежных блоков для улучшения локальности ссылок. В таких случаях даже связанная структура, такая как дерево или связанный список, может быть сделана удобной для кэша при условии, что структура ее узлов является смежной по своей природе. Это эффективно делает разыменование указателя более дешевым, хотя и косвенным путем, улучшая местность ссылок, используемых при разыменовании.
В погоне за указателями
Предположим, у нас есть односвязный список, например:
Foo->Bar->Baz->null
Проблема состоит в том, что если мы выделим все эти узлы отдельно от общего распределителя (и, возможно, не все сразу), фактическая память может быть распределена примерно так (упрощенная схема):
Когда мы начинаем гоняться за указателями и получать доступ к Foo
узлу, мы начинаем с принудительного промаха (и, возможно, сбоя страницы), перемещая порцию из области памяти из более медленных областей памяти в более быстрые области памяти, например так:
Это заставляет нас кэшировать (возможно, также страницы) область памяти только для того, чтобы получить доступ к ее части и изгнать оставшуюся часть, когда мы преследуем указатели вокруг этого списка. Однако, взяв под контроль распределитель памяти, мы можем выделить такой список непрерывно, например:
... и тем самым значительно повысить скорость, с которой мы можем разыменовывать эти указатели и обрабатывать их. Таким образом, хотя и очень косвенно, мы можем ускорить доступ к указателю таким образом. Конечно, если бы мы просто хранили их непрерывно в массиве, у нас не было бы этой проблемы, во-первых, но распределитель памяти здесь, дающий нам явный контроль над макетом памяти, может спасти тот день, когда требуется связанная структура.
* Примечание: это очень упрощенная схема и обсуждение иерархии памяти и местоположения ссылок, но, надеюсь, она соответствует уровню вопроса.