Проблемы, специфичные для языка C ++
Прежде всего, не существует так называемого выделения стека или кучи, предписанного C ++ . Если вы говорите об автоматических объектах в блочных областях, они даже не «выделяются». (Кстати, продолжительность автоматического хранения в 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) на значение, не меньшее требуемого размера кадра. Затем кадр называется выделеннымкогда указатель стека после операций. Параметры функций также могут передаваться в кадр стека, в зависимости от соглашения о вызове, используемого для вызова. Кадр может содержать память автоматических объектов (возможно, включая параметры), указанных в исходном коде C ++. В смысле таких реализаций эти объекты «выделяются». Когда элемент управления выходит из вызова функции, кадр больше не нужен, он обычно освобождается путем восстановления указателя стека обратно в состояние перед вызовом (сохраненное ранее в соответствии с соглашением о вызовах). Это можно рассматривать как «освобождение». Эти операции фактически делают запись активации структурой данных LIFO, поэтому ее часто называют « стеком (вызова) ».
Поскольку большинство реализаций C ++ (особенно те, которые нацелены на собственный код уровня ISA и используют язык ассемблера в качестве непосредственного вывода), используют подобные стратегии, подобные этой, такая запутанная схема «выделения» популярна. Такое распределение (а также освобождение) тратит машинные циклы, и это может быть дорогостоящим, когда (неоптимизированные) вызовы происходят часто, даже если современные микроархитектуры ЦП могут иметь сложные оптимизации, реализованные аппаратно для общего шаблона кода (например, с использованием составлять движок во внедрении PUSH
/ POP
инструкции).
Но в любом случае, в общем, верно, что стоимость выделения кадров стека значительно меньше, чем вызов функции распределения, работающей со свободным хранилищем (если она полностью не оптимизирована) , которая сама может иметь сотни (если не миллионы). :-) операции по поддержанию указателя стека и других состояний. Функции распределения обычно основаны на API, предоставляемом размещенной средой (например, среда выполнения, предоставляемая ОС). В отличие от цели хранения автоматических объектов для вызовов функций, такие распределения являются универсальными, поэтому они не будут иметь структуру кадра, как стек. Традиционно они выделяют пространство из хранилища пула, называемого кучей (или несколькими кучами). В отличие от «стека», понятие «куча» здесь не указывает на используемую структуру данных;это получено из ранних языковых реализаций десятилетия назад, (Кстати, стек вызовов обычно выделяется с фиксированным или заданным пользователем размером из кучи средой при запуске программы или потока.) Характер вариантов использования делает распределение и освобождение из кучи гораздо более сложным (чем push или pop of кадры стека), и вряд ли можно напрямую оптимизировать аппаратно.
Влияние на доступ к памяти
Обычное распределение стека всегда помещает новый фрейм сверху, поэтому он имеет довольно хорошую локализацию. Это дружественно к кешу. OTOH, память, случайно распределенная в бесплатном магазине, не имеет такого свойства. Начиная с ISO C ++ 17, существуют шаблоны ресурсов пула, предоставляемые <memory>
. Непосредственная цель такого интерфейса - сделать так, чтобы результаты последовательных распределений были близки друг другу в памяти. Это признает тот факт, что эта стратегия в целом хороша для производительности с современными реализациями, например, является дружественной к кешу в современных архитектурах. Это касается производительности доступа, а не распределения .
совпадение
Ожидание одновременного доступа к памяти может иметь различные эффекты между стеком и кучами. Стек вызовов обычно принадлежит только одному потоку выполнения в реализации C ++. OTOH, кучи часто распределяются между потоками в процессе. Для таких куч функции распределения и освобождения должны защищать общую внутреннюю административную структуру данных от гонки данных. В результате выделения кучи и освобождения могут иметь дополнительные издержки из-за операций внутренней синхронизации.
Космическая эффективность
Из-за характера сценариев использования и внутренних структур данных, кучи могут страдать от фрагментации внутренней памяти , а стек - нет. Это не оказывает прямого влияния на производительность выделения памяти, но в системе с виртуальной памятью низкая эффективность использования пространства может ухудшить общую производительность доступа к памяти. Это особенно ужасно, когда жесткий диск используется для подкачки физической памяти. Это может вызвать довольно длительную задержку - иногда миллиарды циклов.
Ограничения распределения стека
Хотя выделение стека часто выше по производительности, чем выделение кучи, в действительности это не означает, что выделение стека всегда может заменить выделение кучи.
Во-первых, нет способа выделить место в стеке размером, указанным во время выполнения, переносимым способом с ISO C ++. Существуют расширения, предоставляемые реализациями, такими как alloca
VLA (массив переменной длины) G ++, но есть причины избегать их. (IIRC, источник Linux недавно исключает использование VLA.) (Также обратите внимание, что ISO C99 действительно имеет обязательный VLA, но ISO C11 делает поддержку необязательной.)
Во-вторых, нет надежного и портативного способа обнаружения исчерпания пространства стека. Это часто называют переполнением стека (хм, этимология этого сайта) , но, возможно, более точно, переполнением стека . В действительности это часто приводит к недопустимому доступу к памяти, а затем состояние программы повреждено (... или, что еще хуже, дыра в безопасности). Фактически, ISO C ++ не имеет понятия «стек» и делает его неопределенным поведением, когда ресурс исчерпан . Будьте осторожны с тем, сколько места нужно оставить для автоматических объектов.
Если пространство в стеке заканчивается, в стеке выделяется слишком много объектов, что может быть вызвано слишком большим количеством активных вызовов функций или неправильным использованием автоматических объектов. Такие случаи могут предполагать наличие ошибок, например, рекурсивный вызов функции без правильных условий выхода.
Тем не менее, иногда требуются глубокие рекурсивные вызовы. В реализациях языков, требующих поддержки несвязанных активных вызовов (где глубина вызовов ограничена только общим объемом памяти), невозможно использовать (современный) собственный стек вызовов непосредственно в качестве записи активации целевого языка, как в типичных реализациях C ++. Чтобы обойти проблему, требуются альтернативные способы построения записей активации. Например, SML / NJ явно выделяет кадры в куче и использует стеки кактусов . Сложное распределение таких кадров записи активации обычно не так быстро, как кадры стека вызовов. Однако, если такие языки будут реализованы в дальнейшем с гарантией правильной хвостовой рекурсиипрямое выделение стека в объектном языке (то есть «объект» в языке не хранится в виде ссылок, а собственные значения примитивов, которые могут быть сопоставлены один к одному с неразделенными объектами C ++), еще сложнее, поскольку потеря производительности в целом. При использовании C ++ для реализации таких языков сложно оценить влияние на производительность.