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

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

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

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

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

Ответы

Ответ 1

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

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

Ответ 2

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

sub esp, 0x10

(Это перемещает указатель стека вниз на 0x10 байт и тем самым "распределяет" эти байты для использования переменной.)

Конечно, размер стека очень, очень конечный, так как вы быстро узнаете, злоупотребляете ли вы распределением стека или пытаетесь выполнить рекурсию: -)

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

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

Ответ 3

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

#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 С++, Digital Mars, ICC и другие компиляторы. Как бы то ни было, распределение кучи требуется в сотни раз дольше, чем распределение стека, и я не вижу ничего полезного в дальнейшем изучении вопроса.

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

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

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

  • Возьмите адрес 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++ не исключил конструкторы.


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

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

#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 не делает ничего полезного и может быть оптимизирован. GCC на моем ноутбуке Linux также замечает, что on_heap не делает ничего полезного и оптимизирует его:

on_stack took 0.000003 seconds
on_heap took 0.000002 seconds
on_stack took 0.000003 seconds
on_heap took 0.000002 seconds

Ответ 4

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

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

Ответ 5

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

Также я согласен с Torbjörn Gyllebring о ожидаемом сроке жизни объектов. Хорошая точка!

Ответ 6

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

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

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

Ответ 10

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

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

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

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

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

Ответ 11

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

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

Ответ 12

Это не более быстрое распределение стека. Вы также много выиграете от использования переменных стека. У них лучшая локальность ссылок. И, наконец, освобождение намного дешевле.

Ответ 13

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

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

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

Ответ 14

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

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

Ответ 15

Проблемы, специфичные для языка 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).

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

Влияние на доступ к памяти

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

Параллелизм

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

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

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

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

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

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

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

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

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

Ответ 16

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

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

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

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

Ответ 17

распределение стека выполняется намного быстрее.

Ответ 18

Как говорили другие, распределение стека обычно намного быстрее.

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

Например, если вы выделяете что-то в стеке, а затем помещаете его в контейнер, было бы лучше выделить в куче и сохранить указатель в контейнере (например, с помощью std:: shared_ptr < > ), То же самое верно, если вы передаете или возвращаете объекты по значению и другие подобные сценарии.

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

Ответ 19

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 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, никакого нажатия не произошло.

Ответ 20

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

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

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

Ответ 21

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

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

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

Ответ 22

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

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

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

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

Ответ 23

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

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

Кетан

Ответ 24

Я бы хотел сказать, что на самом деле генерируется код 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 изменится на другое значение. Если у вас не было локальной переменной, эту инструкцию просто нужно выполнить. Поэтому выделение локальной переменной не имеет накладных расходов.