Почему эта неиспользованная переменная не оптимизирована?
Я играл с Godbolt CompilerExplorer. Я хотел видеть, насколько хороши определенные оптимизации. Мой минимальный рабочий пример:
#include <vector>
int foo() {
std::vector<int> v {1, 2, 3, 4, 5};
return v[4];
}
Сгенерированный ассемблер (по clang 5.0.0, -O2 -std = С++ 14):
foo(): # @foo()
push rax
mov edi, 20
call operator new(unsigned long)
mov rdi, rax
call operator delete(void*)
mov eax, 5
pop rcx
ret
Как видно, clang знает ответ, но перед возвращением достаточно много материала. Мне кажется, что даже вектор создан из-за "оператора new/delete".
Может кто-нибудь объяснить мне, что здесь происходит и почему он не просто вернулся?
Код, созданный GCC (не скопированный здесь), как представляется, явно создает вектор. Кто-нибудь знает, что GCC не способен вывести результат?
Ответы
Ответ 1
std::vector<T>
- довольно сложный класс, который включает динамическое распределение. Хотя clang++
иногда может элиминировать распределения кучи, это довольно сложная оптимизация, и вы не должны полагаться на нее. Пример:
int foo() {
int* p = new int{5};
return *p;
}
foo(): # @foo()
mov eax, 5
ret
В качестве примера, используя std::array<T>
(который не динамически выделяется) создает полностью встроенный код:
#include <array>
int foo() {
std::array v{1, 2, 3, 4, 5};
return v[4];
}
foo(): # @foo()
mov eax, 5
ret
Как Марк Глисс отметил в других комментариях ответа, это то, что Стандарт говорит в [ expr.new] # 10:
Реализации разрешено опускать вызов сменной глобальной функции распределения ([new.delete.single], [new.delete.array]). Когда это делается, хранилище вместо этого обеспечивается реализацией или обеспечивается расширением выделения другого нового выражения. Реализация может расширить выделение нового выражения e1 для обеспечения хранения для нового выражения e2, если бы следующее было правдой: распределение не было расширено: [...]
Ответ 2
Как отмечают комментарии, operator new
можно заменить. Это может случиться в любом модуле перевода. Оптимизация программы для случая, который она не заменила, требует комплексного анализа. И если он заменен, вы должны называть его, конечно.
Не задан ли по умолчанию operator new
вызов библиотеки I/O. Это важно, так как вызовы библиотеки ввода-вывода являются наблюдаемыми, и поэтому они также не могут быть оптимизированы.
Ответ 3
N3664 изменить на [expr.new], процитированный в одном ответе и одном комментарии, разрешает новым выражениям не вызывать сменная глобальная функция распределения. Но vector
выделяет память с помощью std::allocator<T>::allocate
, которая вызывает ::operator new
напрямую, а не через новое выражение. Так что специальное разрешение не применяется, и, как правило, компиляторы не могут исключить такие прямые вызовы ::operator new
.
Вся надежда не потеряна, однако для спецификации std::allocator<T>::allocate
this сказать:
Примечания: хранилище получается путем вызова ::operator new
, но оно не указано, когда или как часто эта функция вызывается.
Используя это разрешение, libС++ std::allocator
использует специальные встроенные функции clang, чтобы указать компилятору, что разрешение разрешено. С -stdlib=libc++
, clang компилирует ваш код до
foo(): # @foo()
mov eax, 5
ret