c++ - это - стэк




Что быстрее: распределение стека или выделение кучи (16)

Проблемы, связанные с языком C ++

Прежде всего, нет так называемого «стека» или «кучи», выделенного C ++ . Если вы говорите об автоматических объектах в области блока, они даже не «распределены». (BTW, время автоматического хранения в C определенно не совпадает с «выделенным», последнее является «динамическим» на языке C ++.) И динамически выделенная память находится в свободном хранилище , не обязательно на «куче», хотя последнее часто является реализацией (по умолчанию).

Хотя в соответствии с абстрактными семантическими правилами автоматические объекты все еще занимают память, соответствующая реализация на C ++ позволяет игнорировать этот факт, когда это может доказать, что это не имеет значения (когда это не изменяет наблюдаемое поведение программы). Это разрешение предоставляется по правилу as-if в ISO C ++, которое также является общим предложением, позволяющим обычные оптимизации (и в ISO C также существует почти такое же правило). Помимо правила as-if, ISO C ++ также имеет правила копирования, позволяющие исключить конкретные создания объектов. Таким образом, вызов конструктора и деструктора опущен. В результате автоматические объекты (если они есть) в этих конструкторах и деструкторах также устраняются по сравнению с наивной абстрактной семантикой, подразумеваемой исходным кодом.

С другой стороны, бесплатное размещение магазинов определенно «выделяется» по дизайну. В соответствии с правилами ISO C ++ такое распределение может быть достигнуто вызовом функции распределения . Однако, поскольку ISO C ++ 14, существует новое (не-as-if) правило, позволяющее в определенных случаях разрешать слияние глобальной функции распределения (т.е. ::operator new ). Таким образом, части операций динамического выделения также могут быть не-op, как в случае с автоматическими объектами.

Функции распределения выделяют ресурсы памяти. Объекты могут быть дополнительно распределены на основе распределения с использованием распределителей. Для автоматических объектов они представлены непосредственно - хотя доступ к основной памяти можно получить и использовать для обеспечения памяти для других объектов (путем размещения new ), но это не имеет большого смысла в качестве бесплатного хранилища, поскольку нет возможности двигаться ресурсы в другом месте.

Все остальные проблемы выходят за рамки C ++. Тем не менее, они могут быть значительными.

О реализации C ++

C ++ не раскрывает активированные записи активации или некоторые виды первоклассных продолжений (например, по известному call/cc ), нет возможности напрямую манипулировать кадрами записи активации - там, где реализации необходимо разместить автоматические объекты. Когда нет (не переносных) взаимодействий с базовой реализацией («родной» переносной код, такой как встроенный ассемблерный код), упущение базового распределения кадров может быть довольно тривиальным. Например, когда вызываемая функция встроена, кадры могут быть эффективно объединены с другими, поэтому нет способа показать, что такое «распределение».

Тем не менее, как только interops соблюдаются, все становится сложным. Типичная реализация C ++ предоставит возможность взаимодействия с ISA (архитектура набора команд) с некоторыми соглашениями о вызовах как двоичной границей, совместно используемой с исходным кодом (машиной уровня ISA). Это будет явно дорогостоящим, особенно при сохранении указателя стека , который часто непосредственно удерживается регистром уровня ISA (возможно, с конкретными машинными инструкциями для доступа). Указатель стека указывает границу верхнего кадра (текущего активного) вызова функции. Когда введен вызов функции, необходим новый кадр, и указатель стека добавляется или вычитается (в зависимости от соглашения ISA) на значение не менее необходимого размера кадра. Затем кадр выделяется, когда указатель стека после операций. Параметры функций также могут быть переданы в стек стека, в зависимости от соглашения о вызове, используемого для вызова. В кадре может храниться память автоматических объектов (возможно, включая параметры), заданные исходным кодом C ++. В смысле таких реализаций эти объекты «распределяются». Когда элемент управления выходит из вызова функции, кадр больше не нужен, его обычно отпускают путем восстановления указателя стека до состояния перед вызовом (сохраненного ранее в соответствии с соглашением о вызове). Это можно рассматривать как «освобождение». Эти операции делают запись активации эффективной структурой данных LIFO, поэтому ее часто называют « стеком (вызова) ». Указатель стека эффективно указывает верхнюю позицию стека.

Поскольку большинство реализаций на C ++ (особенно те, которые нацелены на собственный код на уровне ISA и используют язык ассемблера в качестве его непосредственного вывода), используют похожие стратегии, подобные этой запутанной схеме распределения. Такие распределения (а также дезадаптация) действительно проводят машинные циклы, и это может быть дорого, когда часто происходят (не оптимизированные) вызовы, даже если современные микроархитекторы процессора могут иметь сложную оптимизацию, реализованную аппаратными средствами для общей схемы кода (например, с использованием для выполнения инструкций PUSH / POP ).

Но в любом случае, в общем, правда, что стоимость распределения кадров стека значительно меньше, чем вызов функции распределения, управляющей свободным хранилищем (если он полностью не оптимизирован) , который сам может иметь сотни (если не миллионы :-) для поддержания указателя стека и других состояний. Функции распределения обычно основаны на API, предоставляемом размещенной средой (например, время выполнения, предоставляемое ОС).В отличие от целей хранения автоматических объектов для вызовов функций такие распределения являются общими, поэтому они не будут иметь структуру кадра, такую ​​как стек. Традиционно они выделяют пространство из хранилища пула, называемого heap (или нескольких куч). В отличие от «стека» понятие «куча» здесь не указывает на используемую структуру данных; он получен из ранних языковых реализаций десятилетий назад . (BTW, стек вызовов обычно выделяется фиксированным или определяемым пользователем размером из кучи средой при запуске программы или потока.) Характер использования делает выделение и освобождение из кучи гораздо более сложным (чем push или pop кадров стека) и вряд ли можно напрямую оптимизировать аппаратное обеспечение.

Эффекты при доступе к памяти

Обычное распределение стека всегда ставит новый фрейм сверху, поэтому он имеет неплохую локальность. Это удобно для кеша. OTOH, память, распределенная случайным образом в свободном хранилище, не обладает таким свойством. Начиная с ISO C ++ 17, существуют шаблоны ресурсов пула <memory>. Прямая цель такого интерфейса заключается в том, чтобы обеспечить одновременное совпадение результатов последовательных распределений в памяти. Это подтверждает тот факт, что эта стратегия в целом хороша для производительности с современными реализациями, например, быть дружественной к кешу в современных архитектурах. Это касается производительности доступа, а не распределения .

совпадение

Ожидание одновременного доступа к памяти может иметь разные эффекты между стеком и кучами. Стек вызовов обычно принадлежит одному потоку исполнения в реализации C ++. OTOH, кучи часто разделяются между потоками процесса. Для таких кучек функции распределения и освобождения должны защищать общую структуру внутренних административных данных от гонки данных. В результате распределения и освобождения кучи могут иметь дополнительные накладные расходы из-за внутренних операций синхронизации.

Эффективность пространства

Из-за характера случаев использования и внутренних структур данных кучи могут страдать от фрагментации внутренней памяти , а в стеке нет. Это не оказывает прямого влияния на производительность распределения памяти, но в системе с виртуальной памятью низкая эффективность пространства может ухудшить общую производительность доступа к памяти. Это особенно ужасно, когда жесткий диск используется как своп физической памяти. Это может вызвать довольно долгую задержку - иногда миллиарды циклов.

Ограничения распределения стека

Хотя распределения стека часто превосходят по производительности, чем распределение кучи в действительности, это, конечно же, не означает, что распределения стека всегда могут заменять распределения кучи.

Во-первых, нет способа выделить пространство в стеке с размером, указанным во время выполнения, переносимым способом с помощью ISO C ++. Существуют расширения, предоставляемые такими реализациями, как allocaи VLA G ++ (массив переменной длины), но есть причины, чтобы избежать их использования. (Источник IIRC, Linux недавно удалил использование VLA.) (Также обратите внимание, что ISO C99 имеет VLA, но ISO C11 поддерживает эту опцию.)

Во-вторых, нет надежного и портативного способа обнаружения истощения пространства стека. Это часто называют переполнением стека (hmm, этимология этого сайта), но, вероятно, более наложенным образом, «переполнение стека». На самом деле это часто приводит к недействительному доступу к памяти, а состояние программы затем повреждается (или, может быть, хуже, дыра в безопасности). На самом деле, ISO C ++ не имеет понятия стека и делает его неопределенным поведением, когда ресурс исчерпан . Будьте осторожны относительно того, сколько места должно быть оставлено для автоматических объектов.

Если пространство стека заканчивается, в стеке слишком много объектов, которые могут быть вызваны слишком большим количеством активных вызовов функций или неправильным использованием автоматических объектов. Такие случаи могут предполагать наличие ошибок, например, вызов рекурсивной функции без правильных условий выхода.

Тем не менее, иногда требуются глубокие рекурсивные вызовы. В реализациях языков, требующих поддержки несвязанных активных вызовов (глубина вызовов ограничена только суммой памяти), невозможно использовать собственный стек вызовов непосредственно в качестве записи активации целевого языка, как типичные реализации C ++. Например, SML/NJ явно выделяет фреймы в куче и использует стеки кактусов . Сложное распределение таких кадров записи активации обычно не является быстрым, как кадры стека вызовов. Однако при дальнейшем внедрении языков с надлежащей хвостовой рекурсией, прямое распределение стека в языке объекта (то есть «объект» в языке не хранится в качестве ссылок, но примитивные значения, которые могут быть взаимно однозначными, сопоставлены с нерасширенными объектами C ++) еще сложнее с более высоким уровнем производительности в генеральный. При использовании C ++ для реализации таких языков трудно оценить влияние производительности.

Этот вопрос может звучать довольно элементарно, но это дискуссия, с которой я работал с другим разработчиком, с которым я работаю.

Я старался, чтобы стек выделял вещи, где мог, вместо кучи, выделяя их. Он разговаривал со мной и наблюдал за моим плечом и прокомментировал, что это не обязательно, потому что они одинаковые.

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

Это поражает меня как нечто, что, вероятно, будет очень зависимым от компилятора. Для этого проекта, в частности, я использую компилятор Metrowerks для архитектуры PPC . Проницательность в этой комбинации была бы наиболее полезной, но, в общем, для GCC и MSVC ++, в чем дело? Является ли распределение кучи не столь высоким, как распределение стека? Разве нет разницы? Или это разница, так что минута становится бессмысленной микро-оптимизацией.


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

Для других приложений, где время не является проблемой, это может быть не так важно, но если вы куча выделяете много, это повлияет на скорость выполнения. Всегда старайтесь использовать стек для недолговечной и часто выделяемой памяти (например, в циклах), и как можно дольше - распределять кучи во время запуска приложения.


Выделение стека - это несколько инструкций, тогда как самый быстрый распределитель кучи rtos, известный мне (TLSF), использует в среднем порядка 150 инструкций. Кроме того, для распределения стека не требуется блокировка, потому что они используют локальное хранилище потоков, что является еще одним огромным выигрышем в производительности. Таким образом, распределение стека может быть на 2-3 порядка быстрее в зависимости от того, насколько сильно многопоточная среда.

В общем случае распределение кучи является вашим последним средством, если вы заботитесь о производительности. Жизнеспособным промежуточным вариантом может быть фиксированный распределитель пула, который также является лишь инструкциями пары и имеет очень мало ресурсов для распределения, поэтому он отлично подходит для небольших объектов фиксированного размера. С другой стороны, он работает только с объектами фиксированного размера, по своей сути не является потокобезопасным и имеет проблемы фрагментации блоков.


Интересная вещь, которую я узнал о Stack vs. Heap Allocation на Xbox 360 Xenon, который может также применяться к другим многоядерным системам, заключается в том, что выделение в куче вызывает критический раздел для остановки всех других ядер, конфликт. Таким образом, в замкнутой петле, Stack Allocation был способом пойти для массивов фиксированного размера, поскольку это предотвращало ларьки.

Это может быть еще одно ускорение для рассмотрения, если вы кодируете multicore / multiproc, поскольку ваше распределение стека будет доступно только для ядра, использующего вашу ограниченную функцию, и это не повлияет на другие ядра / процессоры.


Как правило, распределение стека быстрее, чем распределение кучи, как упоминалось почти в каждом ответе выше. Выталкивание или выпадение стека O (1), тогда как выделение или освобождение от кучи может потребовать перехода предыдущих распределений. Однако вы не должны выделяться в жестких, интенсивных циклах, поэтому выбор обычно сводится к другим факторам.

Возможно, было бы хорошо сделать это различие: вы можете использовать «распределитель стека» в куче. Строго говоря, я беру выделение стека для обозначения фактического метода распределения, а не местоположения выделения. Если вы выделяете много материала в стеке фактических программ, это может быть плохо по целому ряду причин. С другой стороны, использование метода стека для распределения по куче, когда это возможно, является лучшим выбором, который вы можете сделать для метода распределения.

Поскольку вы упомянули Metrowerks и PPC, я предполагаю, что вы имеете в виду Wii. В этом случае память имеет премиум-память и, используя метод распределения стека, гарантирует, что вы не тратите память на фрагменты. Конечно, для этого требуется гораздо больше внимания, чем «обычные» методы распределения кучи. Целесообразно оценивать компромиссы для каждой ситуации.


Обратите внимание, что соображения, как правило, не касаются скорости и производительности при выборе стека и распределения кучи. Стек действует как стек, что означает, что он хорошо подходит для нажатия блоков и повторения их снова, последний раз, сначала. Выполнение процедур также похоже на стек, последняя введенная процедура сначала должна быть завершена. В большинстве языков программирования все переменные, необходимые в процедуре, будут видны только во время выполнения процедуры, поэтому их вводят при вводе процедуры и выталкивают стек при выходе или возврате.

Теперь для примера, где стек нельзя использовать:

Proc P
{
  pointer x;
  Proc S
  {
    pointer y;
    y = allocate_some_data();
    x = y;
  }
}

Если вы выделите некоторую память в процедуре S и поместите ее в стек, а затем выйдете из S, выделенные данные будут удалены из стека. Но переменная x в P также указывала на эти данные, поэтому x теперь указывает на какое-то место под указателем стека (предположим, что стек растет вниз) с неизвестным контентом. Содержимое может по-прежнему присутствовать, если указатель стека просто перемещается вверх, не очищая данные под ним, но если вы начнете выделять новые данные в стеке, указатель x может фактически указывать на эти новые данные.


Помимо преимуществ производительности по порядку величины по сравнению с распределением кучи, распределение стека предпочтительнее для длинных серверных приложений. Даже самые лучшие управляемые кучи в конечном итоге настолько фрагментированы, что производительность приложения ухудшается.


Ранее упоминалось, что распределение стека просто перемещает указатель стека, то есть одну инструкцию на большинстве архитектур. Сравните это с тем, что обычно происходит в случае распределения кучи.

Операционная система поддерживает части свободной памяти в качестве связанного списка с данными полезной нагрузки, состоящими из указателя на начальный адрес свободной части и размера свободной части. Чтобы выделить X-байты памяти, список ссылок перемещается, и каждая заметка посещается в последовательности, проверяя, является ли ее размер как минимум X. Когда найдена часть с размером P> = X, P разбивается на две части с размеры X и PX. Связанный список обновляется, и возвращается указатель на первую часть.

Как вы можете видеть, распределение кучи зависит от возможных факторов, таких как объем памяти, который вы запрашиваете, как фрагментирована память и так далее.


Распределение стека почти всегда будет таким же быстрым или быстрым, чем распределение кучи, хотя, конечно, для распределителя кучи можно просто использовать технику выделения на основе стека.

Тем не менее, есть большие проблемы при работе с общей производительностью распределения стека и кучи (или в несколько лучших условиях, локальное и внешнее распределение). Обычно распределение кучи (внешнего) происходит медленно, поскольку оно имеет дело со многими различными типами распределения и шаблонами распределения. Уменьшение объема используемого вами распределителя (что делает его локальным для алгоритма / кода) будет способствовать повышению производительности без каких-либо серьезных изменений. Добавление лучшей структуры к вашим шаблонам распределения, например, принудительное упорядочение LIFO по парам распределения и освобождения может также улучшить производительность распределителя, используя распределитель более простым и структурированным способом. Или вы можете использовать или написать распределитель, настроенный для вашего конкретного шаблона распределения; большинство программ часто выделяют несколько дискретных размеров, поэтому куча, основанная на буфере просмотра нескольких фиксированных (предпочтительно известных) размеров, будет работать очень хорошо. Именно по этой причине Windows использует свою низкоразрушающую кучу.

С другой стороны, распределение на основе стека в 32-битном диапазоне памяти также чревато опасностью, если у вас слишком много потоков. Для стеков требуется непрерывный диапазон памяти, поэтому чем больше потоков у вас есть, тем больше виртуального пространства адресов вам потребуется для запуска без переполнения стека. Это не будет проблемой (на данный момент) с 64-разрядной версией, но это может привести к хаосу в длинных программах с большим количеством потоков. Запуск виртуального адресного пространства из-за фрагментации - это всегда боль.


Стек имеет ограниченную емкость, а куча - нет. Типичный стек для процесса или потока составляет около 8K. Вы не можете изменить размер после его выделения.

Переменная стека следует правилам охвата, а кучи - нет. Если указатель инструкции выходит за пределы функции, все новые переменные, связанные с функцией, уходят.

Самое главное, вы не можете заранее предсказать общую цепочку вызовов функций. Таким образом, выделение всего 200 байтов с вашей стороны может привести к переполнению стека. Это особенно важно, если вы пишете библиотеку, а не приложение.


Существует общая точка зрения на такие оптимизации.

Оптимизация, которую вы получаете, пропорциональна количеству времени, в течение которого счетчик программ фактически находится в этом коде.

Если вы попробуете счетчик программ, вы узнаете, где он проводит свое время, и это обычно находится в крошечной части кода, и часто в библиотечных программах вы не контролируете.

Только если вы обнаружите, что он тратит много времени на выделение кучи ваших объектов, будет заметно быстрее, если они будут распределены по стеклу.


Честно говоря, тривиально написать программу для сравнения производительности:

#include <ctime>
#include <iostream>

namespace {
    class empty { }; // even empty classes take up 1 byte of space, minimum
}

int main()
{
    std::clock_t start = std::clock();
    for (int i = 0; i < 100000; ++i)
        empty e;
    std::clock_t duration = std::clock() - start;
    std::cout << "stack allocation took " << duration << " clock ticks\n";
    start = std::clock();
    for (int i = 0; i < 100000; ++i) {
        empty* e = new empty;
        delete e;
    };
    duration = std::clock() - start;
    std::cout << "heap allocation took " << duration << " clock ticks\n";
}

Говорят, что глупая консистенция - это хобгоблин маленьких умов . По-видимому, оптимизация компиляторов - это хоббиглины умов многих программистов. Это обсуждение было в основе ответа, но люди, по-видимому, не могут потрудиться, чтобы это так далеко, поэтому я перехожу сюда, чтобы избежать вопросов, на которые я уже ответил.

Оптимизирующий компилятор может заметить, что этот код ничего не делает и может оптимизировать все это. Задача оптимизатора состоит в том, чтобы делать такие вещи, и борьба с оптимизатором - это безумное поручение.

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

Любой, кто включит оптимизатор, а затем жалуется на борьбу с ним, должен подвергаться публичным насмешкам.

Если бы я заботился о наносекундной точности, я бы не использовал std::clock() . Если бы я хотел опубликовать результаты в качестве докторской диссертации, я бы сделал большую сделку по этому поводу, и я бы, вероятно, сравнил GCC, Tendra / Ten15, LLVM, Watcom, Borland, Visual C ++, Digital Mars, ICC и другие компиляторы. Как бы то ни было, распределение кучи требуется в сотни раз дольше, чем распределение стека, и я не вижу ничего полезного в дальнейшем изучении вопроса.

У оптимизатора есть задача избавиться от кода, который я тестирую. Я не вижу причин, чтобы сказать, что оптимизатор запускается, а затем попытаться обмануть оптимизатора, фактически не оптимизируя. Но если бы я увидел ценность в этом, я бы сделал одно или несколько из следующего:

  1. Добавить элемент данных для empty и получить доступ к этому элементу данных в цикле; но если я только когда-либо прочитал из элемента данных, оптимизатор может делать постоянную фальцовку и удалять петлю; если я только когда-либо напишу члену данных, оптимизатор может пропустить все, кроме самой последней итерации цикла. Кроме того, вопрос заключался не в «распределении стека и доступе к данным против распределения кучи и доступа к данным».

  2. Объявлять e volatile , но volatile часто компилируется неправильно (PDF).

  3. Возьмите адрес e внутри цикла (и, возможно, назначьте его переменной, объявленной extern и определенной в другом файле). Но даже в этом случае компилятор может заметить, что - по крайней мере в стеке - e всегда будет выделяться по одному и тому же адресу памяти, а затем делать постоянную фальцовку, как в (1) выше. Я получаю все итерации цикла, но объект никогда не выделяется.

Помимо очевидного, этот тест имеет недостатки в том, что он измеряет как распределение, так и освобождение, а исходный вопрос не спрашивает об освобождении. Конечно, переменные, выделенные в стеке, автоматически освобождаются в конце своей области, поэтому не вызывать delete (1) перекосить числа (освобождение стека включено в числа о распределении стека, поэтому справедливо оценивать освобождение кучи) и (2) вызывают довольно плохую утечку памяти, если мы не сохраним ссылку на новый указатель и не delete вызов после того, как у нас получится измерение времени.

На моей машине, используя g ++ 3.4.4 в Windows, я получаю «0 тактов» для распределения стека и кучи для чего-либо менее 100000 распределений, и даже тогда я получаю «0 тактов времени» для распределения стека и «15 тактов «для распределения кучи. Когда я измеряю 10 000 000 распределений, распределение стека занимает 31 такт, а распределение кучи занимает 1562 такта.

Да, оптимизирующий компилятор может ускорить создание пустых объектов. Если я правильно понимаю, он может даже превысить весь первый цикл. Когда я натолкнулся на итерации до 10 000 000 распределений стека, ушло 31 такт, а распределение кучи заняло 1562 такта. Я думаю, что можно с уверенностью сказать, что, не сообщив g ++ об оптимизации исполняемого файла, g ++ не исключил конструкторы.

За годы, прошедшие с того момента, как я написал это, предпочтение от заключалось в том, чтобы опубликовать производительность оптимизированных сборок. В общем, я думаю, что это правильно. Тем не менее, я по-прежнему думаю, что глупо просить компилятор оптимизировать код, когда вы на самом деле не хотите, чтобы этот код оптимизирован. Мне кажется, что я очень похож на оплату дополнительной парковки автомобилей, но отказываюсь сдавать ключи. В этом конкретном случае я не хочу, чтобы оптимизатор работал.

Используя слегка измененную версию эталона (чтобы указать допустимую точку, что исходная программа не выделяла что-либо в стеке каждый раз через цикл) и компиляции без оптимизации, но связываясь с релизными библиотеками (чтобы обратиться к действительной точке, которую мы надеваем 't хочу включить любое замедление, вызванное связыванием с библиотеками отладки):

#include <cstdio>
#include <chrono>

namespace {
    void on_stack()
    {
        int i;
    }

    void on_heap()
    {
        int* i = new int;
        delete i;
    }
}

int main()
{
    auto begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_stack();
    auto end = std::chrono::system_clock::now();

    std::printf("on_stack took %f seconds\n", std::chrono::duration<double>(end - begin).count());

    begin = std::chrono::system_clock::now();
    for (int i = 0; i < 1000000000; ++i)
        on_heap();
    end = std::chrono::system_clock::now();

    std::printf("on_heap took %f seconds\n", std::chrono::duration<double>(end - begin).count());
    return 0;
}

дисплеи:

on_stack took 2.070003 seconds
on_heap took 57.980081 seconds

в моей системе при компиляции с командной строкой cl foo.cc /Od /MT /EHsc .

Вы можете не согласиться с моим подходом к получению не оптимизированной сборки. Все в порядке: не стесняйтесь модифицировать бенчмарк столько, сколько хотите. Когда я включаю оптимизацию, я получаю:

on_stack took 0.000000 seconds
on_heap took 51.608723 seconds

Не потому, что распределение стека фактически мгновенно, но потому, что любой on_stack компилятор может заметить, что on_stack ничего полезного и не может быть оптимизирован. GCC на моем ноутбуке Linux также замечает, что on_heap не делает ничего полезного и оптимизирует его:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

Я думаю, что жизнь имеет решающее значение, и нужно ли строить сложную вещь. Например, при моделировании, основанном на транзакциях, вам обычно необходимо заполнить и передать структуру транзакций с кучей полей для функций работы. Посмотрите на стандарт OSCI SystemC TLM-2.0.

Выделение их в стеке близко к вызову операции, как правило, приводит к огромным издержкам, поскольку строительство дорого. Хороший способ состоит в том, чтобы выделять кучу и повторно использовать объекты транзакции путем объединения или простой политики, например, «для этого модуля требуется только один объект транзакции».

Это во много раз быстрее, чем выделение объекта при каждом вызове операции.

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

Я бы сказал: попробуйте оба и посмотрите, что лучше всего работает в вашем случае, потому что это действительно может зависеть от поведения вашего кода.


Я не думаю, что распределение стека и распределение кучи обычно взаимозаменяемы. Я также надеюсь, что производительность обоих из них достаточно для общего использования.

Я настоятельно рекомендую для небольших предметов, в зависимости от того, какой из них больше подходит для сферы распределения. Для больших предметов куча, вероятно, необходима.

В 32-разрядных операционных системах, которые имеют несколько потоков, стеки часто довольно ограничены (хотя обычно, по крайней мере, несколько мб), поскольку адресное пространство должно быть вырезано, и рано или поздно один поток стека будет запущен в другой. В однопоточных системах (в любом случае, Linux glibc однопоточно) ограничение намного меньше, потому что стек может просто расти и расти.

В 64-разрядных операционных системах достаточно адресного пространства, чтобы сделать стеки потоков довольно большими.


Я бы хотел сказать, что на самом деле генерация кода GCC (я тоже помню VS) не имеет накладных расходов для распределения стека .

Произнесите следующую функцию:

  int f(int i)
  {
      if (i > 0)
      {   
          int array[1000];
      }   
  }

Ниже приводится генерация кода:

  __Z1fi:
  Leh_func_begin1:
      pushq   %rbp
  Ltmp0:
      movq    %rsp, %rbp
  Ltmp1:
      subq    $**3880**, %rsp <--- here we have the array allocated, even the if doesn't excited.
  Ltmp2:
      movl    %edi, -4(%rbp)
      movl    -8(%rbp), %eax
      addq    $3880, %rsp
      popq    %rbp
      ret 
  Leh_func_end1:

Так что, сколько локальных переменных у вас есть (даже внутри if или switch), только 3880 изменится на другое значение. Если у вас не было локальной переменной, эту инструкцию просто нужно выполнить. Поэтому выделение локальной переменной не имеет накладных расходов.


class Foo {
public:
    Foo(int a) {

    }
}
int func() {
    int a1, a2;
    std::cin >> a1;
    std::cin >> a2;

    Foo f1(a1);
    __asm push a1;
    __asm lea ecx, [this];
    __asm call Foo::Foo(int);

    Foo* f2 = new Foo(a2);
    __asm push sizeof(Foo);
    __asm call operator new;//there's a lot instruction here(depends on system)
    __asm push a2;
    __asm call Foo::Foo(int);

    delete f2;
}

Это было бы так в asm. Когда вы находитесь в func , f1 и указатель f2 выделены в стеке (автоматическое хранилище). И, кстати, Foo f1(a1) не имеет эффектов команды на указатель стека ( esp ), он был выделен, если func хочет получить член f1 , его инструкция выглядит примерно так: lea ecx [ebp+f1], call Foo::SomeFunc() . Еще одна вещь, которую выделяет стек, может заставить кого-то думать, что память - это что-то вроде FIFO , FIFO только что произошло, когда вы входите в какую-то функцию, если вы находитесь в функции и выделяете что-то вроде int i = 0 , никакого толчка не произошло.







heap