RVO(返回值优化)是不是适用于所有对象?
Posted
技术标签:
【中文标题】RVO(返回值优化)是不是适用于所有对象?【英文标题】:Is RVO (Return Value Optimization) applicable for all objects?RVO(返回值优化)是否适用于所有对象? 【发布时间】:2011-11-27 14:42:21 【问题描述】:RVO (Return Value Optimization) 是否保证或适用于 C++ 编译器(特别是 GCC)中的所有对象和情况?
如果答案是“否”,那么对类/对象进行这种优化的条件是什么?如何强制或鼓励编译器对特定返回值执行 RVO?
【问题讨论】:
【参考方案1】:gcc 编译器中的所有对象都保证 RVO(返回值优化)吗?
永远保证没有优化(尽管 RVO 相当可靠,但确实存在 some cases that throw it off)。
如果答案是“否”,那么这个类/对象的优化条件是什么?
一个从你那里刻意抽象出来的实现细节。
请不要知道也不关心这个。
【讨论】:
您的最后一点有点像 Java 语言的评论.. ;) 知道没有害处,它可能是特定于实现的,但如果有这种倾向 - 为什么不呢?但是,至于关心,只有在分析中突出它... @Nim:想知道正在执行哪些优化是“错误的”。好吧,很公平,如果你真的为某些关键任务系统收紧你的程序,那么分析可能会引导你走上黑暗艺术的道路,具体了解你的编译器正在做什么,但这不应该成为默认方法。 @Nim:因为这些条件随时可能改变。它们实际上并不可靠。 RVO 可以一直应用,NRVO是有时不能应用的。 对于很多(非关键任务)应用程序,“在大多数情况下应该优化”就足够了,因此知道它通常何时执行的原因。如果在大多数情况下通过对程序稍加修改就能获得相当大的性能提升,为什么不呢?【参考方案2】:我没有是或否的答案,但你说如果你正在寻找的优化得到保证,你可以编写更少的代码行。
如果你写了你需要写的代码,程序就会一直运行,如果优化在那里,它会运行得更快。如果确实存在优化在逻辑中“填补空白”而不是代码机制并使其工作的情况,或者彻底改变逻辑,那似乎是我想要修复的错误而不是我想依赖或利用的实现细节。
【讨论】:
我不介意投反对票,但请解释原因。我不明白说“请不要让你的代码依赖于优化”有什么问题。 我没有对你投反对票,但我不确定这是否真的回答了这个问题。它有点扫视它。 :) 这很公平。有时答案真的是“请上帝不要这样做”,尽管我认为这个问题远非如此。 @MasoudM。说我疯了,但我仍然认为,如果缺少某种优化,则无法正常工作的代码被破坏了。进行优化的原因是为了加速逻辑,而不是“修复”它。这与知道优化发生在哪里并简单地编写代码来定位它们不同——即使优化丢失或被禁用,你的代码仍然可以工作。【参考方案3】:返回值优化可以总是应用,不能普遍应用的是命名返回值优化。基本上,为了进行优化,编译器必须知道在构造对象的地方将返回什么对象。
在 RVO(返回一个临时值)的情况下,该条件很容易满足:对象是在 return 语句中构造的,并且它被返回了。
对于 NRVO,您必须分析代码以了解编译器是否知道该信息。如果函数的分析很简单,编译器很可能会对其进行优化(例如,不包含条件的单个 return 语句;同一对象的多个 return 语句;编译器知道的多个 return 语句,如T f() if (condition) T r; return r; else T r2; return r2;
r
或 r2
将被退回...)
请注意,您只能在简单的情况下假设优化,具体来说,***中的示例实际上可以由足够智能的编译器进行优化:
std::string f( bool x )
std::string a("a"), b("b");
if ( x ) return a;
else return b;
可以被编译器改写成:
std::string f( bool x )
if ( x )
std::string a("a"), b("b");
return a;
else
std::string a("a"), b("b");
return b;
此时编译器可以知道在第一个分支中a
将被构造来代替返回的对象,并且在第二个分支中同样适用于b
。但我不会指望这一点。如果代码很复杂,假设编译器无法产生优化。
编辑:有一种情况我没有明确提到,不允许编译器(在大多数情况下即使允许,它也不可能这样做)优化掉副本从函数的参数到返回语句:
T f( T value ) return value; // Cannot be optimized away --but can be converted into
// a move operation if available.
【讨论】:
最好更正***文章及其引用的参考资料,包括 Hinnant 文章。 ***文章没有错误,示例中的注释明确指出RVO 可能不适用。我只是指出,根据编译器,甚至可以避免重要的副本。 请注意,RVO/NRVO 仅适用于返回 临时对象,即复制后通常会被破坏的对象。例如,从成员函数返回成员变量时,必须进行复制。这对某些人来说似乎很明显,但是当他们刚刚了解 RVO 时,我不得不向他们解释这一点。 @DavidRodríguez-dribeas 你能解释一下你的最后一个例子吗?value
不是通过值传递的临时方式,因此完全符合 RVO 的条件吗?
@Owen: value
不会超过这个功能,但它不是暂时的(没关系)。该语言没有规定要删除该副本,这就是注释的原因。至于为什么,复制省略是通过直接在副本将拥有的内存上创建副本来完成的,那么就不需要复制了。常见的 ABI 让调用者为返回的对象保留空间,它也是复制参数的调用者。但是在单独编译的语言中,调用者不知道函数是否会返回该参数 [...]【参考方案4】:
致 Jesper:如果要构建的对象很大,则可能需要避免复制(或至少非常可取)。
如果发生 RVO,则避免复制,您无需再编写任何代码行。
如果没有,您将不得不手动完成,自己编写额外的脚手架。这可能会涉及到提前指定一个缓冲区,迫使你为这个空的(可能是无效的,你可以看到它不干净)对象编写一个构造函数和一个“构造”这个无效对象的方法。
因此,如果有保证,它可以减少我的代码行数。不是吗?”并不意味着马苏德是个白痴。然而,对他来说不幸的是,不能保证 RVO。您必须测试它是否发生,如果没有,编写脚手架并污染您的设计。不可能是疱疹的。
【讨论】:
【参考方案5】:移动语义(C++11 的新特性)是您问题的解决方案,它允许您显式使用Type(Type &&r);
(移动构造函数),而不是Type(const Type &r)
(复制构造函数)。
例如:
class String
public:
char *buffer;
String(const char *s)
int n = strlen(s) + 1;
buffer = new char[n];
memcpy(buffer, s, n);
~String() delete [] buffer;
String(const String &r)
// traditional copy ...
String(String &&r)
buffer = r.buffer; // O(1), No copying, saves time.
r.buffer = 0;
;
String hello(bool world)
if (world)
return String("Hello, world.");
else
return String("Hello.");
int main()
String foo = hello();
std::cout <<foo.buffer <<std::endl;
而且这不会触发复制构造函数。
【讨论】:
-1 因为如果你使用 return std::move(temp_object) 对象可能会超出范围(简单的例子,T t; return std::move(t);)你会留下一个无效的参考。当您在此示例中使用 String 时,它可能会起作用,因为字符串保存在池中,即使超出范围也可能不会被删除,但通常不信任它。 @WorldSEnder 我在这里没有看到无效的参考。return
-ing 对象会使其构造一个新对象(通过复制或移动),而不是临时对象的引用。你说的只有当返回类型是引用或指针时才会发生。
不返回String&&是对的。谢谢,我认为它是不同的。
对于仍然在此答案中遇到问题的任何人,请注意您应该不在返回声明中使用std::move
。这样做没有任何好处,实际上可能prevent the compiler 可以利用 RVO。具有讽刺意味的是,这回答了“RVO 是否适用于所有对象?”的问题。通过产生一个不允许 RVO 的情况。
@Brian 我稍微编辑了答案。嗯...,那是几年前的事了,我不了解诸如 pr 值、x 值之类的机制。现在我们有了 C++17(很快就会有 C++20),其中复制省略是完全标准化的,我可能不会提及移动语义。但是,此答案旨在处理无法保证 RVO 的情况。以上是关于RVO(返回值优化)是不是适用于所有对象?的主要内容,如果未能解决你的问题,请参考以下文章