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

问题描述 投票:65回答:7

我目前正在研究如何编写高效的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)

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

c++ performance function return-value pass-by-reference
7个回答
23
投票

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

  1. 如果需要返回简单或基本对象,则两种情况下的性能都相似。
  2. 如果对象是如此庞大和复杂,返回它将需要一个副本,它可能比将它作为引用参数慢,但它会花费更少的内存,我认为。

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


8
投票

那么,必须明白编译不是一件容易的事。编译器编译代码时需要考虑很多因素。

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

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

对于非托管编译,答案并不简单。当我想到“XXX会比YYY跑得更快吗?”时会浮现几个问题,例如:

  • 你是默认构造的对象吗?
  • 您的编译器是否支持返回值优化?
  • 您的对象是否支持仅复制语义,还是支持复制和移动?
  • 对象是以有条不紊的方式打包的(例如std::array)还是指向堆上的东西? (例如std::vector)?

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

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


7
投票

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

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

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


5
投票

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

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

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

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

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

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


3
投票

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

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

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

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

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


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

1
投票

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.