Может ли компилятор оптимизировать распределение от кучи до стека?

Что касается оптимизации компилятора, допустимо и / или возможно ли изменить выделение кучи на выделение стека? Или это нарушит правило «как если бы»?

Например, предположим, что это исходная версия кода.

{
    Foo* f = new Foo();
    f->do_something();
    delete f;
}

Сможет ли компилятор изменить это на следующее

{
    Foo f{};
    f.do_something();
}

Я бы так не подумал, потому что это имело бы последствия, если бы исходная версия полагалась на такие вещи, как настраиваемые распределители. В стандарте что-то конкретно говорится об этом?


person Cory Kramer    schedule 02.11.2017    source источник
comment
Нет, это заходит слишком далеко. Растущее использование стека - это большое дело, они назвали в его честь популярный веб-сайт, посвященный программированию.   -  person Hans Passant    schedule 02.11.2017
comment
По теме.   -  person nwp    schedule 02.11.2017
comment
Иногда есть очень веские причины использовать кучу / стек; в частности для встраиваемых систем.   -  person UKMonkey    schedule 02.11.2017
comment
связанные: stackoverflow .com / questions / 47072261 /   -  person 463035818_is_not_a_number    schedule 02.11.2017
comment
Clang оптимизирует это, если он может встроить вызываемую функцию (+ возможно, некоторые условия в теле функции). godbolt.org/g/hnAMTZ   -  person Baum mit Augen    schedule 02.11.2017
comment
из ссылки, упомянутой tobi303, со времени c ++ 14 все изменилось, см. [expr .new]; начиная с C ++ 14 компилятор может хранить Foo в стеке, если он может доказать такое же поведение (например, в do_something ничего не выбрасывается)   -  person Massimiliano Janes    schedule 02.11.2017
comment
@UKMonkey Хороший момент, но можно представить себе ситуацию, когда f даже не будет выделен вообще, потому что его элементы данных в конечном итоге окажутся в регистрах или полностью оптимизированы. В этом случае у динамического выделения нет никаких преимуществ, за исключением очевидного факта, что запрошенное выделение кучи никогда не выполнялось. Можно было бы разумно спросить, сможет ли компилятор затем устранить это.   -  person user4815162342    schedule 02.11.2017
comment
@MassimilianoJanes: Я только что просмотрел expr.new и не видел ограничений относительно того, бросает ли do_something. Вы видите что-то запрещающее замену новым, когда do_something выкидывает? Если да, может, стоит включить в свой ответ.   -  person geza    schedule 02.11.2017
comment
@geza, как я читал, реализации разрешено опускать выделение, но нельзя произвольно добавлять новые побочные эффекты как следствие такого упущения; то есть он может хранить Foo в стеке, но не может вызывать деструктор, если do_something () выбрасывает; следовательно, два фрагмента кода не эквивалентны, если компилятор не может доказать, что do_something не бросает   -  person Massimiliano Janes    schedule 02.11.2017
comment
@MassimilianoJanes; Ах я вижу. Думаю, я включу эту информацию в свой ответ, спасибо!   -  person geza    schedule 02.11.2017


Ответы (3)


Да, это законно. expr.new/10 из C ++ 14:

Реализации разрешено опускать вызов заменяемой функции глобального распределения (18.6.1.1, 18.6.1.2). При этом хранилище вместо этого предоставляется реализацией или предоставляется путем расширения выделения другого выражения new-expression.

expr.delete/7:

Если значение операнда выражения удаления не является значением нулевого указателя, то:

- Если вызов выделения для нового-выражения для удаляемого объекта не был пропущен и выделение не было расширено (5.3.4), выражение-удаление должно вызывать функцию освобождения (3.7.4.2). Значение, возвращаемое из вызова выделения нового выражения, должно быть передано в качестве первого аргумента функции освобождения.

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

- В противном случае выражение удаления не вызовет функцию освобождения (3.7.4.2).

Итак, в общем, можно заменить new и delete чем-то определенным в реализации, например, с использованием стека вместо кучи.

Примечание: как комментирует Массимилиано Джейнс, компилятор не может точно придерживаться этого преобразования для вашего образца, если do_something выдает: компилятор должен опустить вызов деструктора f в этом случае (в то время как ваш преобразованный образец действительно вызывает деструктор в этом случае). Но кроме этого, можно бесплатно поместить f в стек.

person geza    schedule 02.11.2017
comment
Вопрос в том, может ли выделение быть в стеке, даже если используется new. Если я правильно понимаю, это всегда будет динамическая память, а не в стеке. В данном абзаце говорится, что размер выделения может быть увеличен до большего блока динамической памяти или использовать предыдущее расширение динамической памяти. - person SHR; 02.11.2017
comment
@SHR: Я сделал акцент на том, что хранилище вместо этого предоставляется реализацией. Это может быть что угодно, даже стек. - person geza; 02.11.2017
comment
Предыстория этих изменений обсуждается в моем ответе на вопрос Разрешено ли компилятору оптимизировать распределение памяти в куче? - person Shafik Yaghmour; 03.11.2017

Они не эквивалентны. f.do_something() может бросить, и в этом случае первый объект останется в памяти, а второй будет уничтожен.

person lorro    schedule 02.11.2017
comment
Стоит отметить, что объявление функции noexcept не помогает оптимизаторам gcc и clang, но показ clang тела функции помогает. Вероятно, это еще не все. - person Baum mit Augen; 02.11.2017
comment
@BaummitAugen Если вы спрашиваете, почему компиляторы не выполняют эту оптимизацию, я думаю, что это действительно нечто большее: когда кто-то пишет новое выражение, он хочет динамического распределения. Если бы они хотели выделить стек, они бы написали Foo f{}. Для этого есть веские причины, и компилятор не может знать, например возможно, они работают под valgrind и хотят отслеживать все использования кучи, или, возможно, они отлаживают проблему фрагментации кучи. Компилятор должен найти баланс между разрешенными оптимизациями и тем, чего действительно хотят кодеры. Компилятор должен быть другом, а не врагом - person M.M; 03.11.2017

Я хотел бы указать на то, что ИМО недостаточно подчеркивает в других ответах:

struct Foo {
    static void * operator new(std::size_t count) {
        std::cout << "Hey ho!" << std::endl;
        return ::operator new(count);
    }
};

Распределение new Foo(), как правило, не может быть заменено, потому что:

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

Таким образом, как и в примере Foo выше, необходимо вызвать Foo::operator new. Пропуск этого вызова изменит наблюдаемое поведение программы.

Пример из реальной жизни: Foos может потребоваться размещение в какой-то специальной области памяти (например, ввод-вывод с отображением памяти) для правильной работы.

person Daniel Jour    schedule 03.11.2017