2008-10-02 3 views
434

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

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

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

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

+9

Я знаю, что это довольно древний, но было бы неплохо увидеть некоторые фрагменты C/C++, демонстрирующие различные виды распределения. – 2011-06-05 15:48:58

+28

Ваш орк коровы ужасно неосведомлен, но более важно, что он опасен, потому что он делает авторитетные заявления о вещах, о которых он ужасно не знает. Акцизируйте таких людей из своей команды как можно быстрее. – 2013-05-19 00:57:22

+4

Обратите внимание, что куча обычно * много * больше, чем стек. Если вам выделены большие объемы данных, вам действительно нужно положить их в кучу или изменить размер стека из ОС. – 2013-11-04 06:00:53

ответ

435

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

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

+185

И, что более важно, стек всегда горячий, память, которую вы получаете, гораздо более вероятна в кеше, чем какая-либо отдаленная память с кучей – 2009-04-10 10:29:06

+43

На некоторых (в основном встроенных, которые я знаю) архитектурах стек может храниться в быстром режиме, (например, SRAM). Это может иметь огромное значение! – leander 2009-07-15 01:16:12

+2

@ Benoît Не могли бы вы объяснить, почему бы просто не хранить все в стеке? Какой смысл кучи? – Pacerier 2012-01-29 00:51:31

16

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

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

+0

Это иногда называют распределением плиты. – Benoit 2013-07-24 08:41:00

5

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

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

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

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

146

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

sub esp, 0x10 

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

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

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

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

5

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

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

3

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

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

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

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

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

3

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

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

3

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

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

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

107

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

#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"; 
} 

Говорят, что a foolish consistency is the hobgoblin of little minds. По-видимому, оптимизация компиляторов - это хоббиглины умов многих программистов. Это обсуждение было в основе ответа, но люди, по-видимому, не могут потрудиться, чтобы это так далеко, поэтому я перехожу сюда, чтобы избежать вопросов, на которые я уже ответил.

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

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

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

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

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

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

  2. Объявление evolatile, but volatile is often compiled incorrectly (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 ++ не исключил конструкторы.


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

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

#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 
3

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

2

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

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

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

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

0

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

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

Ketan

1

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

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

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

1

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

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

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

26

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

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

6

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

3

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

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

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

3

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

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

2

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

1

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

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

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

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

1

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

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

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

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

-1

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

Say для следующей функции:

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: 

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

2
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, там не было никакого толчка.

Смежные вопросы