在 C++/C++11 中,哪种 "return" 方法更适用于大数据?
Which "return" method is better for large data in C++/C++11?
这个问题是由于对 C++11 中的 RVO 的混淆而引发的。
我有两种方法来 "return" 值:return 通过值 和 return 通过参考参数。如果我不考虑性能,我更喜欢第一个。由于按值 return 更自然,我可以轻松区分输入和输出。但是,如果我考虑 return 大数据时的效率。我不能决定,因为在C++11中,有RVO。
这是我的示例代码,这两个代码做同样的工作:
return 按值
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;
}
return 通过参考参数
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
}
}
这是我的问题:
- 我如何确保 RVO 发生在 C++11 中?
- 如果我们确定发生了RVO,在现在的C++编程中,您推荐哪种"return"方法?为什么?
- 为什么有些库通过引用参数、代码风格或历史原因使用return?
更新
由于这些答案,我知道第一种方法在大多数方面更好。
这里有一些有用的相关链接,可以帮助我理解这个问题:
- In C++, is it still bad practice to return a vector from a function?
- Want Speed? Pass by Value.
首先,您正在做的事情的正确技术术语是 NRVO。 RVO 与临时 returned:
有关
X foo() {
return make_x();
}
NRVO 指的是被 returned:
命名的对象
X foo() {
X x = make_x();
x.do_stuff();
return x;
}
其次,(N)RVO是编译器优化,不是强制的。但是,您可以非常确定,如果您使用现代编译器,(N)RVO 将被非常积极地使用。
第三,(N)RVO 不是 C++11 特性 - 它早在 2011 年就存在了。
第四,您在 C++11 中拥有的是 move 构造函数。因此,如果您的 class 支持移动语义,它将被移动,而不是被复制,即使 (N)RVO 没有发生。不幸的是,并不是所有的东西都可以有效地进行语义移动。
第五,return 引用是一个可怕的反模式。它确保对象将被有效地创建两次 - 第一次作为 'empty' 对象,第二次在填充数据时 - 并且它阻止你使用 'empty' 状态不是有效不变量的对象。
无法确保 C++11 中出现 RVO(或 NVRO)。无论是否发生,它都与实现质量(例如编译器)有关,而不是由程序员从根本上控制的。
在某些情况下可以使用移动语义来实现类似的效果,但与 RVO 不同。
一般来说,我建议使用适用于手头数据的任何 return 方法,程序员可以理解。程序员可以理解的代码更容易正确工作。使用神秘的技术来优化性能(例如试图强制 NVRO 发生)往往会使代码更难理解,因此更容易出错(例如增加未定义行为的可能性)。如果代码工作正常,但测量显示它缺乏所需的性能,那么可以探索更多神秘的技术来提高性能。但是,出于某种原因,尝试预先亲切地手动优化代码(即在任何测量提供需要的证据之前)被称为 "premature optimisation"。
通过引用返回可以避免函数在 return 上复制大量数据。因此,如果函数是 returning 大型数据结构,returning by reference 比 returning by value 更有效(通过各种措施)。不过,这需要权衡取舍——return如果底层数据不复存在而其他代码引用了它,那么引用某物是危险的(导致未定义的行为)。然而,returning 一个值使得某些代码很难保存对(例如)可能已不复存在的数据结构的引用。
编辑:添加示例,其中 return 引用是危险的,如评论中所要求的。
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()
return 是对在它 return 之后不再存在的东西的引用 - 通常称为悬挂引用。因此,对该引用的任何使用(在这种情况下,打印引用的值)都具有未定义的行为。按值返回(即简单地删除 &
)returns 变量的副本,当调用者尝试使用它时它存在。
未定义行为的原因是 func()
returns。但是,未定义的行为将发生在调用者(使用引用)中而不是 func()
本身。因果分离会导致错误很难追踪。
SergyA 的回答很完美。如果您遵循该建议,您几乎总是不会出错。
但是有一种 'result' 最好从调用站点传递对结果的引用。
这是您在循环中使用 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);
}
}
这个问题是由于对 C++11 中的 RVO 的混淆而引发的。
我有两种方法来 "return" 值:return 通过值 和 return 通过参考参数。如果我不考虑性能,我更喜欢第一个。由于按值 return 更自然,我可以轻松区分输入和输出。但是,如果我考虑 return 大数据时的效率。我不能决定,因为在C++11中,有RVO。
这是我的示例代码,这两个代码做同样的工作:
return 按值
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;
}
return 通过参考参数
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
}
}
这是我的问题:
- 我如何确保 RVO 发生在 C++11 中?
- 如果我们确定发生了RVO,在现在的C++编程中,您推荐哪种"return"方法?为什么?
- 为什么有些库通过引用参数、代码风格或历史原因使用return?
更新 由于这些答案,我知道第一种方法在大多数方面更好。
这里有一些有用的相关链接,可以帮助我理解这个问题:
- In C++, is it still bad practice to return a vector from a function?
- Want Speed? Pass by Value.
首先,您正在做的事情的正确技术术语是 NRVO。 RVO 与临时 returned:
有关X foo() {
return make_x();
}
NRVO 指的是被 returned:
命名的对象X foo() {
X x = make_x();
x.do_stuff();
return x;
}
其次,(N)RVO是编译器优化,不是强制的。但是,您可以非常确定,如果您使用现代编译器,(N)RVO 将被非常积极地使用。
第三,(N)RVO 不是 C++11 特性 - 它早在 2011 年就存在了。
第四,您在 C++11 中拥有的是 move 构造函数。因此,如果您的 class 支持移动语义,它将被移动,而不是被复制,即使 (N)RVO 没有发生。不幸的是,并不是所有的东西都可以有效地进行语义移动。
第五,return 引用是一个可怕的反模式。它确保对象将被有效地创建两次 - 第一次作为 'empty' 对象,第二次在填充数据时 - 并且它阻止你使用 'empty' 状态不是有效不变量的对象。
无法确保 C++11 中出现 RVO(或 NVRO)。无论是否发生,它都与实现质量(例如编译器)有关,而不是由程序员从根本上控制的。
在某些情况下可以使用移动语义来实现类似的效果,但与 RVO 不同。
一般来说,我建议使用适用于手头数据的任何 return 方法,程序员可以理解。程序员可以理解的代码更容易正确工作。使用神秘的技术来优化性能(例如试图强制 NVRO 发生)往往会使代码更难理解,因此更容易出错(例如增加未定义行为的可能性)。如果代码工作正常,但测量显示它缺乏所需的性能,那么可以探索更多神秘的技术来提高性能。但是,出于某种原因,尝试预先亲切地手动优化代码(即在任何测量提供需要的证据之前)被称为 "premature optimisation"。
通过引用返回可以避免函数在 return 上复制大量数据。因此,如果函数是 returning 大型数据结构,returning by reference 比 returning by value 更有效(通过各种措施)。不过,这需要权衡取舍——return如果底层数据不复存在而其他代码引用了它,那么引用某物是危险的(导致未定义的行为)。然而,returning 一个值使得某些代码很难保存对(例如)可能已不复存在的数据结构的引用。
编辑:添加示例,其中 return 引用是危险的,如评论中所要求的。
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()
return 是对在它 return 之后不再存在的东西的引用 - 通常称为悬挂引用。因此,对该引用的任何使用(在这种情况下,打印引用的值)都具有未定义的行为。按值返回(即简单地删除 &
)returns 变量的副本,当调用者尝试使用它时它存在。
未定义行为的原因是 func()
returns。但是,未定义的行为将发生在调用者(使用引用)中而不是 func()
本身。因果分离会导致错误很难追踪。
SergyA 的回答很完美。如果您遵循该建议,您几乎总是不会出错。
但是有一种 'result' 最好从调用站点传递对结果的引用。
这是您在循环中使用 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);
}
}