C++ 以最佳方式返回复制的值
我很惊讶地看到Jonathan Wakely在libstdc++中引入了以下优化:并尝试对其进行研究 对于任何非平凡类,给定concat函数的两个实现,令人惊讶的是,只有第一个是最优的(一个副本c-tor和一个运算符+=调用)。其他人需要额外的复制/移动c-tor呼叫-您可以在godbolt.org上进行验证 为什么呢?我假设所有这些实现都将使用-O2生成相同的代码 语言规范是否规定了此类行为(如果是,为什么?),或者这是一个QoI问题 这是所有版本和语言版本上GCC和Clang之间的一致行为C++ 以最佳方式返回复制的值,c++,C++,我很惊讶地看到Jonathan Wakely在libstdc++中引入了以下优化:并尝试对其进行研究 对于任何非平凡类,给定concat函数的两个实现,令人惊讶的是,只有第一个是最优的(一个副本c-tor和一个运算符+=调用)。其他人需要额外的复制/移动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);
}
更新:修改代码以排除以下运算符+=”实现的问题:
myclass& myclass::operator+=(myclass const& v) {
static myclass weird;
return weird;
}
考虑这一点:
myclass& myclass::operator+=(myclass const& v) {
static myclass weird;
return weird;
}
这完全合法
您的推理基于我们认为理所当然的一点:+=运算符将始终返回*this
编译器不允许做出这种假设
在concat()
中,这无关紧要,因为我们强制concat()
的返回值为+=操作的myclass
的实例。在所有其他情况下,编译器必须假设情况更糟,除非它可以保证+=返回<代码> *< < /> > .< /p> 复制删除(RVO,NRVO,和一个特殊的初始化情况)是一个非常特殊的优化:它是C++中唯一没有被AS-IF规则覆盖的优化。对于每一次其他优化,编译器只需以一种不改变可观察行为、只改变性能的方式修改程序。但是RVO可以改变可观察的行为,因为省略的复制构造函数和析构函数调用(如输出)中的副作用会丢失。这就是为什么标准中有一个特殊的许可证,允许编译器在非常特定的情况下执行此优化
具体来说,RVO仅在返回值为prvalue时适用,NRVO仅在返回表达式引用局部变量时适用
语句returncopy+=rhs中的返回表达式代码>两者都不是。它并不是简单地引用局部变量(它是一个复合赋值表达式,而不是id表达式),返回值也不是prvalue(您的+=
重载返回一个左值引用,使该值成为左值;如果运算符按值返回,这将是不同的,但否则将非常不规则)
您可能认为编译器可以内联运算符并发现它是同一个对象,但在此级别上没有授予执行此优化的权限。我记得最近观看的一次演讲中,在return语句中需要复制省略
语句
是局部变量的名称,您可以通过键入return(x)来阻止它
而不是返回x代码>。不幸的是,我记不起是哪一次谈话了,我也不能给出任何其他好的参考。我想是这一次:不,不是这一次。如果将myclass更改为以下,则会得到相同的行为:`void extern_copy();无效外部移动();无效外部加上赋值();结构myclass{myclass();myclass(const myclass&){extern_copy();}myclass(myclass&){extern_move();}myclass&operator=(const myclass&);myclass&operator=(myclass&);myclass&operator+=(const myclass&){extern_plus_assign();返回*this;}`@Maciejcancora啊,我很惊讶编译器没能解决这个问题。潜在的问题仍然是引用不是prvalues保持不变。是否要详细说明“引用不是prvalues”问题?在CutAT5函数<代码> CutAT5IMPL(LHS,RHS)< /Cord>表达式为PROVALL(右?),如果我正确理解,这个行为由C++标准强制执行。您是否看到RVO/NRVO规则无法扩展到上述情况的原因?(返回copy+=rhs),这将需要进行跨函数分析,并对单独的编译和可能的编译时间造成严重破坏。我不明白为什么它会像您所说的那样有问题。如果编译器有可用的函数定义,那么它们已经执行了跨函数分析(例如内联),并且由于RVO/NRVO已经是可选的,所以当函数定义不可用时,它们就不能执行此优化。@MaciejConcora它们这样做是为了优化,而不是为了语言规则。从C++17开始,复制省略是强制性的。不,编译器现在不/不能执行这种优化,因为它们不允许按语言规则进行优化(优化不能改变可观察的行为,RVO/NRVO是该规则的例外)。当表达式返回copy+=rhs时,C++17不改变任何内容代码>。