【问题标题】:Optimal way to return local value in C++11在 C++11 中返回局部值的最佳方法
【发布时间】:2013-08-29 03:14:52
【问题描述】:

在过去,如果我想要一个对象A 的字符串表示,我会写一些带有签名void to_string(const A& a, string& out) 的东西以避免额外的副本。这仍然是 C++11 中的最佳实践,包括移动语义和所有内容吗?

我在其他上下文中阅读了一些建议依赖 RVO 而不是写 string to_string(const A& a) 的 cmets。但 RVO 不保证会发生!那么,作为 to_string 的程序员,我如何保证字符串不会被不必要地复制(独立于编译器)?

【问题讨论】:

  • 这在很长一段时间内都不是最佳做法(甚至在移动之前)。
  • 如果不能使用 RVO,那么编译器将首先尝试在默认情况下返回复制之前移动。别担心,顺其自然地写东西。
  • 如果你必须保证它,那么你必须使用参考。但是,我知道没有不实现 RVO 的编译器。如果这样的野兽确实存在,那么它生成的代码的性能就不太重要了。无论哪种方式,只需使用 RVO。
  • 完整的答案将分布在多个答案和 cmets 中,因此我创建了一个包含自包含答案的社区 wiki 帖子

标签: c++11 copy move-semantics return-by-reference return-by-value


【解决方案1】:

这是我从反馈和其他资源中收集到的答案:

简单的按值返回是成语,因为:

  • 在实践中,复制/移动省略大部分时间都会发生;
  • move ctor 将用于回退;
  • 防止不太可能发生的副本实际发生不值得编写可读性较差的代码
  • 传入引用要求对象已经创建
    • 并不总是可行的(例如,可能没有默认的 ctor)并且
    • 如果问题是性能,还必须考虑一次初始化过多

但是,如果预计典型用法类似于

std::string s;
while (i_need_to)
{
    to_string(get_A(), s);
    process(s);
    update(i_need_to);
}

并且如果所讨论的类型具有默认构造函数*,那么传递应该通过引用保存返回的对象可能仍然有意义。

*此处仅考虑字符串作为示例,但问题和答案可以概括

【讨论】:

    【解决方案2】:

    在过去(1970-1980 年),您几乎可以通过计算浮点除数来预测算法的性能。

    今天不再是这样。但是,您可以使用类似的规则来估计今天的性能:

    计算到堆的次数:new/mallocdelete/free

    给定:

    std::string
    to_string(const A& a)
    {
        std::string s;
        // fill it up
        return s;
    }
    
    std::string s = test();
    

    假设您没有将 s 内部重新分配给 to_string(),我算了 1 个新的。当您将数据放入s 时,就完成了这一分配。我知道std::string 有一个快速(无分配)移动构造函数。因此,RVO 是否发生与估计to_string() 的性能无关。在to_string() 之外创建s 将有1 个分配。

    现在考虑:

    void
    to_string(const A& a, string& out)
    {
        out = ...
    }
    
    std::string s;
    to_string(a, s);
    

    正如我所写,它仍然消耗 1 个内存分配。所以这与按值返回版本的速度差不多。

    现在考虑一个新的用例:

    while (i_need_to)
    {
        std::string s = to_string(get_A());
        process(s);
        update(i_need_to);
    }
    

    根据我们之前的分析,上面将在每次迭代中进行 1 次分配。现在考虑一下:

    std::string s;
    while (i_need_to)
    {
        to_string(get_A(), s);
        process(s);
        update(i_need_to);
    }
    

    我知道stringcapacity(),并且该容量可以在上述循环中的许多用途中回收。最坏的情况是每次迭代我仍然有 1 个分配。最好的情况是第一次迭代将创建足够大的容量以处理所有其他迭代,并且整个循环将只执行 1 次分配。

    真相可能介于最坏和最好的情况之间。

    最好的 API 取决于您认为您的函数最有可能出现的用例。

    计算分配以估计性能。然后测量你编码的内容。在std::string 的情况下,可能会有一个短字符串缓冲区可能会影响您的决定。对于 libc++,在 64 位平台上,std::string 在访问堆之前将存储多达 22 个 char(加上终止的 null)。

    【讨论】:

      【解决方案3】:

      假设你的函数中的代码是这样的:

      std::string data = ...;
      //do some processing.
      return data;
      

      如果省略不可用,则需要调用std::string 的移动构造函数。所以最坏的情况是,你可以从你的内部字符串中转移出来。

      如果您负担不起移动操作的费用,则必须将其作为参考传递。

      话虽如此...您是否担心编译器无法内联短函数?您是否担心小型包装器是否无法正确优化?编译器不优化 for 循环等的可能性是否困扰您?有没有想过if(x < y)是否比if(x - y < 0)快?

      如果不是...那您为什么要关心复制/移动省略(“返回值优化”的技术术语,因为它在更多地方使用)?如果您使用的编译器不支持复制省略,那么您使用的编译器可能无法支持大量其他优化。出于性能考虑,您最好花时间升级编译器,而不是将返回值转换为引用。

      防止复制的不太可能的情况实际发生是不值得的......麻烦?可读性较差的代码?究竟是什么?除了简单的回报,还有什么额外的东西?

      “额外的东西”是这样的:

      std::string aString = to_string(a);
      

      比这更具可读性:

      std::string aString;
      to_string(a, aString);
      

      在第一种情况下,to_string 很明显立即正在初始化一个字符串。在第二个中,它不是;您必须查看 to_string 的签名以查看它是否使用了 const 引用。

      第一种情况甚至不是“惯用的”;这就是每个人通常会这样写的方式。你永远不会看到to_int(a, someInt) 调用整数;这是荒谬的。为什么整数创建和对象创建如此不同?作为程序员,您不应该必须关心是否为返回值或某事发生了太多的副本。您只需以简单、明显且易于理解的方式做事即可。

      【讨论】:

      • +1 “为了性能,你最好花时间升级你的编译器,而不是把返回值变成引用。”说得好! :)
      • 我并不担心任何特定的性能案例,也没有使用奇怪的编译器。只是试图从一般意义上理解成语是什么以及为什么。而且我确实对循环展开的性能影响或为什么 ++i 比 i++ 更好感兴趣。当然,一旦我确定了一个推理,我就可以理解我只是使用它,而不是每次都考虑所有细节。但我确实更愿意先了解原因,因此提出了问题
      • 我从答案、cmets 和我在其他地方读到的内容可以理解的是,直接按值返回是成语,因为:在实践中,复制/移动省略大部分时间都会发生; move ctor 将用于回退;防止实际发生的不太可能的副本情况不值得……麻烦吗?可读性较差的代码?究竟是什么?简单回报方面的额外因素是什么?因为如果你传递一个非常量引用,代码的可读性似乎并没有降低,也不是很麻烦......
      • @ricab: (A) 如果有一个移动赋值运算符,那么就不可能发生复制。 (B) 如果您没有从中受益,为什么还要编写令人困惑且可读性较差的代码?人们直观地期望get_name 什么都不带,并返回一个类似对象的字符串。人们并不直观地期望get_name 要求您给它一个字符串。您可能已经习惯了,但这并不意味着它很直观。这很令人困惑。你已经习惯了。
      • @ricab: 不,如果函数是update_height,你应该给它提供一个非常量引用,要么什么都不返回,要么可能是一些信息。
      猜你喜欢
      • 1970-01-01
      • 2020-09-03
      • 1970-01-01
      • 2016-07-02
      • 2013-05-18
      • 1970-01-01
      • 2019-05-10
      • 1970-01-01
      • 2012-12-28
      相关资源
      最近更新 更多