哪个更有效:返回一个值与通过引用传递?

时间:2015-11-30 09:14:55

标签: c++ performance function return-value pass-by-reference

我目前正在研究如何编写高效的C ++代码,而在函数调用方面,我想到了一个问题。比较这个伪代码函数:

not-void function-name () {
    do-something
    return value;
}
int main () {
    ...
    arg = function-name();
    ...
}

使用其他相同的伪代码函数:

void function-name (not-void& arg) {
    do-something
    arg = value;
}
int main () {
    ...
    function-name(arg);
    ...
}

哪个版本更有效率,在什么方面(时间,内存等)?如果它取决于,那么第一个什么时候会更有效率,什么时候第二个会更有效?

编辑:对于上下文,此问题仅限于与硬件平台无关的差异,并且大多数情况也仅限于软件。是否有任何与机器无关的性能差异?

编辑:我看不出这是怎么回事。另一个问题是比较传递参考(上游代码)和传递价值(下面):

not-void function-name (not-void arg)

这跟我的问题不一样。我的重点不在于将参数传递给函数的更好方法。我的重点是哪个是将 out 结果从外部范围传递给变量的更好方法。

7 个答案:

答案 0 :(得分:23)

首先,请注意返回一个对象总是比通过引用传递更具可读性(并且性能非常相似),因此对于您的项目返回对象并提高可读性而言可能更有趣重要的性能差异。 如果你想知道如何获得最低的成本,那么你需要回报的是:

  1. 如果您需要返回简单或基本对象,两种情况下的表现都相似。

  2. 如果对象是如此庞大和复杂,返回它需要一个副本,它可能比将它作为引用参数慢,但它会花费更少的内存我认为。

  3. 无论如何,你必须考虑编译器做了很多优化,使两个表演非常相似。请参阅Copy Elision

答案 1 :(得分:8)

嗯,必须明白编译不是一件简单的事情。编译器编译代码时需要考虑很多因素。

人们不能简单地回答这个问题,因为C ++标准不提供标准的ABI(抽象二进制接口),因此允许每个编译器编译任何它喜欢的代码,并且每次编译都可以得到不同的结果。 / p>

例如,在某些项目中,C ++被编译为Microsoft CLR(C ++ / CX)的托管扩展。因为所有的东西都已经有了对堆上对象的引用,我想没有区别。

对于未经管理的编辑,答案并不简单。当我想到“XXX会比YYY跑得更快吗?”时会想到几个问题,例如:

  • 你是否反对建构性?
  • 您的编译器是否支持返回值优化?
  • 您的对象是否支持仅复制语义,还是支持复制和移动?
  • 对象是否以有条不紊的方式打包(例如std::array),或者它指向堆上的某些内容? (例如std::vector)?

如果我举一个具体的例子,我的猜测是在MSVC ++和GCC上,按值返回std::vector将是通过引用传递它,因为r值优化,并且将是(几纳秒)然后通过移动返回向量。例如,这在Clang上可能完全不同。

最终,剖析是唯一真正的答案。

答案 2 :(得分:6)

在大多数情况下,应该使用返回对象,因为有一个名为copy elision的操作。

但是,根据您的函数的使用方式,最好通过引用传递对象。

例如,请查看std::getline,其中引用std::string。此函数旨在用作循环条件并保持填充std::string直到达到EOF。使用相同的std::string允许在每次循环迭代中重用std::string的存储空间,从而大大减少需要执行的内存分配的数量。

答案 3 :(得分:5)

有些答案涉及到这一点,但我想根据编辑强调

  

对于上下文,此问题仅限于与硬件平台无关的差异,并且大多数情况下也仅限于软件。是否有任何与机器无关的性能差异?

如果这是问题的限制,答案是没有答案。 c ++规范没有规定如何以性能方式实现对象的返回或通过引用的传递,只是它们在代码方面的语义。

因此,编译器可以自由地优化一个与另一个相同的代码,假设这不会给程序员带来明显的差异。

鉴于此,我认为最好使用最直观的情况。如果函数确实是“返回”一个对象作为某个任务或查询的结果,则返回它,如果该函数正在对外部代码所拥有的某个对象执行操作,则通过引用传递。

你无法概括性能。首先,做一些直观的事情,看看你的目标系统和编译器如何优化它。如果在分析后发现问题,请根据需要进行更改。

答案 4 :(得分:3)

我们不能100%一般,因为不同的平台有不同的ABI,但我认为我们可以做一些相当一般的声明,适用于大多数实现,但需要注意的是这些事情主要适用于未内联的函数。

首先让我们考虑原始类型。在低级别,通过引用传递参数是使用指针实现的,而原始返回值通常在寄存器中按字面传递。因此,返回值可能会更好。在某些架构上,这同样适用于小型结构。复制一个足够小的值以适合一个或两个寄存器非常便宜。

现在让我们考虑更大但仍然简单(没有默认构造函数,复制构造函数等)返回值。通常,通过将函数指向应该放置返回值的位置来处理更大的返回值。复制省略允许从函数返回的变量,临时用于返回,调用者中放置结果的变量合并为一个。因此传递的基础与传递参考和返回值大致相同。

总的来说,对于原始类型,我希望返回值略微更好,对于更大但仍然很简单的类型,我希望它们相同或更好,除非你的编译器在复制省略上非常糟糕。

对于使用默认构造函数的类型,复制构造函数等事情会变得更复杂。如果多次调用该函数,则返回值将强制每次重新构造对象,而参考参数可以允许重用数据结构而不进行重构。另一方面,参考参数将在调用函数之前强制(可能不必要)构造。

答案 5 :(得分:2)

这个伪代码功能:

not-void function-name () {
    do-something
    return value;
}
当返回值不需要对其进行任何进一步修改时,可以更好地使用

。传递的参数仅在function-name中修改。没有更多的参考资料。

其他相同的伪代码功能:

void function-name (not-void& arg) {
    do-something
    arg = value;
}
如果我们有另一个方法来调节同一个变量的值,那么

会很有用,我们需要通过调用保持对变量所做的更改。

void another-function-name (not-void& arg) {
    do-something
    arg = value;
}

答案 6 :(得分:1)

在性能方面,副本通常更昂贵,尽管小对象的差异可能微不足道。此外,您的编译器可能会将返回副本优化为移动,这相当于传递引用。

我建议不要传递非const引用,除非你有充分的理由。使用返回值(例如tryGet()排序的函数)。

如果你想要,你可以衡量自己的差异,正如其他人已经说过的那样。对两个版本运行测试代码几百万次,看看差异。