Оптимальное возвращение скопированного значения
Я был довольно удивлен, увидев, что Джонатан Вакели, оптимизированный в libstdc++, оптимизирован: https://gcc.gnu.org/ml/libstdc++/2018-05/txtc2I3IxLCfn.txt и попытался расследовать это.
Учитывая пару реализаций concat-функции для любого нетривиального класса, удивительно, что только первый является оптимальным (один экземпляр c-tor и один operator+ = вызов). Другие требуют дополнительного копирования/перемещения вызова c-tor - вы можете проверить его на godbolt.org.
Это почему? Я бы предположил, что вся эта реализация будет генерировать тот же код с -O2.
Указывает ли спецификация языка такое поведение (если да, почему?) Или это проблема QoI?
Это согласованное поведение между GCC и Clang для всех версий и языковых версий.
void extern_copy();
void extern_move();
void extern_plus_assign();
struct myclass
{
myclass(const myclass&)
{
extern_copy();
}
myclass(myclass&&)
{
extern_move();
}
myclass& operator+=(const myclass&)
{
extern_plus_assign();
return *this;
}
};
myclass concat(const myclass& lhs, const myclass& rhs)
{
myclass copy(lhs);
copy += rhs;
return copy;
}
myclass concat2(const myclass& lhs, const myclass& rhs)
{
myclass copy(lhs);
return copy += rhs;
}
myclass concat3(const myclass& lhs, const myclass& rhs)
{
return myclass(lhs) += rhs;
}
static myclass concat4impl(myclass lhs, const myclass& rhs)
{
return lhs += rhs;
}
myclass concat4(const myclass& lhs, const myclass& rhs)
{
return concat4impl(lhs, rhs);
}
static myclass concat5impl(myclass lhs, const myclass& rhs)
{
lhs += rhs;
return lhs;
}
myclass concat5(const myclass& lhs, const myclass& rhs)
{
return concat5impl(lhs, rhs);
}
Обновление: код изменен, чтобы исключить проблему с последующей реализацией operator+ =:
myclass& myclass::operator+=(myclass const& v) {
static myclass weird;
return weird;
}
Ответы
Ответ 1
Копирование elision (RVO, NRVO и специальный случай инициализации) является очень специальной оптимизацией: это единственная оптимизация в C++, которая не распространяется на правило AS-IF. При каждой другой оптимизации компилятор просто модифицирует программу таким образом, чтобы не изменять наблюдаемое поведение, а только производительность. Но RVO может изменять наблюдаемое поведение, потому что побочные эффекты в конструкторах со списком исключенных копий и деструкторах (например, вывод) теряются. Вот почему в стандарте для специального компилятора есть специальная лицензия для выполнения этой оптимизации при очень специфических обстоятельствах.
В частности, RVO применяется только в том случае, если возвращаемое значение является значением prvalue, а NRVO применяется только в том случае, если возвращаемое выражение тривиально относится к локальной переменной.
Возвращаемое выражение в выражении return copy += rhs;
нет. Это не тривиально относится к локальной переменной (это выражение составного присваивания, а не id-expression), а возвращаемое значение не является значением prvalue (ваш +=
перегрузка возвращает ссылку lvalue, делая значение lvalue; это будет разный, если оператор возвращается по значению, но это будет сильно унииоматично в противном случае).
Вы можете подумать, что компилятор может встроить оператор и обнаружить, что он тот же объект, но разрешение на эту оптимизацию не предоставляется на этом уровне.
Ответ 2
Учти это:
myclass& myclass::operator+=(myclass const& v) {
static myclass weird;
return weird;
}
Это совершенно законно!
Ваши рассуждения основаны на том, что мы считаем само собой разумеющимся: что операторы + = всегда будут возвращать *this
.
Компилятору не разрешено делать это предположение.
В concat()
это не имеет значения, поскольку мы вынуждаем возвращаемое значение concat()
быть экземпляром myclass
на котором работает + =. Во всех остальных случаях компилятор должен принять худшее, если он не может гарантировать, что + = возвращает *this
.