Почему эта неиспользованная переменная не оптимизирована?

Я играл с 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