对于C ++ / C ++ 11中的大数据,哪种“返回”方法更好?

时间:2016-05-12 13:15:05

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

这个问题是由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. 我如何确保在C ++ 11中发生了RVO?
  2. 如果我们确定RVO已经发生,那么在现在的C ++编程中,哪种“返回”方法你推荐?为什么?
  3. 为什么有些库使用返回引用参数,代码样式或历史原因?
  4. 更新 感谢这些答案,我知道第一种方法在大多数方面都更好。

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

    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.

3 个答案:

答案 0 :(得分:16)

首先,您所做的正确技术术语是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没有发生,它也将被移动而不是被复制。不幸的是,并非一切都可以在语义上有效地移动。

第五,通过引用返回是一个可怕的反模式。它确保对象有效地创建两次 - 第一次是“空”&#39;对象,第二次填充数据时 - 它会阻止您使用“清空”的对象。 state不是有效的不变量。

答案 1 :(得分:3)

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);
    }
}

答案 2 :(得分:1)

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

在某些情况下可以使用移动语义来实现类似的效果,但与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()返回对返回后不再存在的内容的引用 - 通常称为悬空引用。因此,对该引用的任何使用(在这种情况下,打印引用的值)都具有未定义的行为。按值返回(即简单地删除&)将返回变量的副本,该变量在调用者尝试使用时存在。

未定义行为的原因是func()返回的原因。但是,未定义的行为将出现在调用者(使用引用)中,而不是func()本身。原因和结果之间的分离可能会导致很难追查的错误。