【问题标题】:Which "return" method is better for large data in C++/C++11?对于 C++/C++11 中的大数据,哪种“返回”方法更好?
【发布时间】:2016-05-12 13:15:04
【问题描述】:

这个问题是由对 C++11 中的 RVO 的混淆引发的。

我有两种“返回”值的方法:按值返回通过引用参数返回。如果我考虑性能,我更喜欢第一个。由于按值返回更自然,我可以轻松区分输入和输出。但是,如果我考虑返回大数据时的效率。我无法决定,因为在 C++11 中,有 RVO。

这是我的示例代码,这两个代码做同样的工作:

按价值返回

struct SolutionType
{
    vector<double> X;
    vector<double> Y;
    SolutionType(int N) : X(N),Y(N) { }
};

SolutionType firstReturnMethod(const double input1,
                               const double input2);
{
    // Some work is here

    SolutionType tmp_solution(N); 
    // since the name is too long, I make alias.
    vector<double> &x = tmp_solution.X;
    vector<double> &y = tmp_solution.Y;

    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }

    return tmp_solution;
}

通过参考参数返回

void secondReturnMethod(SolutionType& solution,
                        const double input1,
                        const double input2);
{
    // Some work is here        

    // since the name is too long, I make alias.
    vector<double> &x = solution.X;
    vector<double> &y = solution.Y;

    for (...)
    {
    // some operation about x and y
    // after that these two vectors become very large
    }
}

这是我的问题:

  1. 我如何确保 RVO 发生在 C++11 中?
  2. 如果我们确定发生了 RVO,在当今的 C++ 编程中,您推荐哪种“返回”方法?为什么?
  3. 为什么有些库使用通过引用参数、代码风格或历史原因返回?

更新 多亏了这些答案,我知道第一种方法在大多数情况下都更好。

这里有一些有用的相关链接可以帮助我理解这个问题:

  1. How to return large data efficiently in C++11
  2. In C++, is it still bad practice to return a vector from a function?
  3. Want Speed? Pass by Value.

【问题讨论】:

    标签: c++ c++11 parameter-passing return-value return-value-optimization


    【解决方案1】:

    首先,正确的技术术语是 NRVO。 RVO 与返回的临时人员有关:

    X foo() {
       return make_x();
    }
    

    NRVO 指的是返回的命名对象:

    X foo() {
        X x = make_x();
        x.do_stuff();
        return x;
    }
    

    其次,(N)RVO 是编译器优化,不是强制性的。但是,您可以非常确定,如果您使用现代编译器,(N)RVO 将会被非常积极地使用。

    第三,(N)RVO 不是 C++11 特性 - 它早在 2011 年就已经出现了。

    首先,您在 C++11 中拥有的是 move 构造函数。因此,如果您的类支持移动语义,它将被移动而不是复制,即使 (N)RVO 没有发生。不幸的是,并非所有内容都可以在语义上有效地移动。

    第五,通过引用返回是一种可怕的反模式。它确保对象将被有效地创建两次 - 第一次作为“空”对象,第二次在填充数据时 - 它阻止您使用“空”状态不是有效不变量的对象。

    【讨论】:

    • 不是fourth的忠实粉丝,嗯?
    • @Zereges 永远不要返回使用return std::move(something); 这是一种悲观。
    • @Zereges using move 强制使用移动或复制构造函数。如果你把它关掉,那么 NRVO 或 RVO 可以启动,返回的变量将直接在调用者空间中构造。
    • @IInspectable,有些人不喜欢13,日本人不喜欢6(如果我没记错的话),我可以成为不喜欢4的人吗? :)) JK。感谢您发现它。
    • @tobi303,我在第五部分的意思是你首先将你的对象创建为一个空对象(构造),然后用数据填充它——这也可以被认为是构造步骤(参见两步构造函数)。这是低效的,但更糟糕的是,它要求你的类有一个默认的构造函数——这在语义上可能不合适。
    【解决方案2】:

    SergyA 的回答是完美的。如果您遵循该建议,您几乎总是不会出错。

    但是,有一种“结果”,最好从调用站点传递对结果的引用。

    这是在您使用 std 容器作为循环中的结果缓冲区的情况下。

    如果您查看函数 std::getline,您会看到一个示例。

    std::getline 旨在从输入流中填充std::string 缓冲区。

    每次使用相同的字符串引用调用 getline 时,字符串的数据都会被覆盖。请注意,随着时间的推移(假设行长度随机),有时需要字符串的隐式reserve 以适应新的长行。但是,比迄今为止最长的行更短的行不需要reserve,因为已经有足够的capacity

    想象一个具有以下签名的 getline 版本:

    std::string fictional_getline(std::istream&);
    

    这意味着每次调用函数时都会返回一个新字符串。无论是否发生 RVO 或 NRVO,都需要创建该字符串,如果它比短字符串优化边界长,则需要分配内存。此外,每次超出范围时,字符串的内存都会被释放。

    在这种情况下,以及其他类似的情况下,将结果容器作为参考传递会更有效。

    例子:

    void do_processing(const std::string& s)
    {
        // ...
    }
    
    /// @post: in the case of an error, os.bad() == true
    /// @post: in the case of no error, os.bad() == false
    std::string fictional_getline(std::istream& stream)
    {
        std::string result;
        if (not std::getline(stream, result))
        {
            // what to do here?
        }
        return result;
    }
    
    // note that buf is re-used which will require fewer and fewer 
    // reallocations the more the loop progresses
    void fast_process(std::istream& stream)
    {
        std::string buf;
        while(std::getline(std::cin, buf))
        {
            do_processing(buf);
        }
    }
    
    // note that buf is re-created and destroyed each time around the loop    
    void not_so_fast_process(std::istream& stream)
    {
        for(;;)
        {
            auto buf = fictional_getline(stream);
            if (!stream) break;
            do_processing(buf);
        }
    }
    

    【讨论】:

    • 感谢您的回答。如果我在循环中创建一个对象,我知道它每次都会创建和破坏,但是如果我将对象清除出循环呢?在not_so_fast_process,我这样做:std::string buf; for(;;){buf=fictional_getline(stream);if (!stream) break;do_processing(buf);},它是否像fast_process 中的buf 一样重用buf
    • @Reigs 允许编译器省略复制构造函数而不管副作用如何,但不允许省略赋值运算符。在这种情况下,每个循环仍然有一个冗余的构造函数/析构函数循环。
    • 我明白了。如果我在循环之外清除 buf,在这一行 buf=fictional_getline(stream); 中,由于它是无法省略的赋值,因此将一直存在副本。所以,效率会低一些。谢谢。
    【解决方案3】:

    没有办法确保 RVO(或 NVRO)出现在 C++11 中。无论是否发生,它都与实现的质量(例如编译器)有关,而不是程序员可以从根本上控制的东西。

    在某些情况下可以使用移动语义来实现类似的效果,但与 RVO 不同。

    一般来说,我建议使用任何适用于手头数据的返回方法,这是程序员可以理解的。程序员可以理解的代码更容易正常工作。使用神秘技术来优化性能(例如,试图强制 NVRO 发生)往往会使代码更难理解,而不是因此更容易出错(例如,增加未定义行为的可能性)。如果代码工作正常,但 MEASUREMENTS 显示它缺乏所需的性能,那么可以探索更多神秘的技术来提高性能。但是,出于某种原因,尝试善意地手动优化代码(即在任何测量提供需要的证据之前)被称为“过早优化”。

    通过引用返回可以避免在函数返回时复制大量数据。因此,如果函数返回一个大型数据结构,则通过引用返回可能比按值返回更有效(通过各种措施)。不过,这需要权衡取舍——如果基础数据不复存在而其他代码引用它,则返回对某事物的引用是危险的(导致未定义的行为)。然而,返回一个值会使某些代码难以保存对(例如)可能已不存在的数据结构的引用。

    编辑:根据评论中的要求,添加通过引用返回是危险的示例。

       AnyType &func()
       {
           Anytype x;
            // initialise x in some way
    
           return x;
       };
    
       int main()
       {
            // assume AnyType can be sent to an ostream this wah
    
            std::cout << func() << '\n';     // undefined behaviour here
       }
    

    在这种情况下,func() 返回一个对在它返回后不再存在的东西的引用——通常称为悬空引用。因此,对该引用的任何使用(在这种情况下,打印引用的值)都有未定义的行为。按值返回(即简单地删除&amp;)返回变量的副本,当调用者尝试使用它时,该副本就存在。

    未定义行为的原因是func() 如何返回。但是,未定义的行为将发生在调用者(使用引用)中,而不是在 func() 本身内。因果之间的分离可能会导致难以追踪的错误。

    【讨论】:

    • 感谢您的回答。我知道,“过早的优化是万恶之源。”而且,我不会尝试优化它。我是 C++ 的新手,当我编写代码时,我只想以正确的方式编写它。你能给我一个“返回对某事的引用是危险的”的例子吗?我认为参考在参数列表中,它确实存在。
    • 好的。我给出了一个返回悬空引用的函数示例。构造类似的示例稍微复杂一些,但仍然很容易,其中一个参数通过引用传递,并且该引用被返回,但被引用的对象在使用之前就不再存在了。
    • 对不起,我可能没有清楚地描述这个问题。在我的问题中,它不是通过引用返回,而是通过引用参数“返回”出来,真正的返回是无效的。我为误导而道歉。
    猜你喜欢
    • 2016-05-10
    • 1970-01-01
    • 2010-10-17
    • 2016-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-27
    • 1970-01-01
    相关资源
    最近更新 更多