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

99

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

这并不是我的问题。 我的重点不在于哪种方法更好地传递参数给函数。 我的重点在于从外部范围传递结果到变量的哪种方式更好。


12
为什么不试一下呢?这可能取决于您的平台和编译器。试着执行一百万次并对其进行分析。总的来说,编写清晰易懂的代码最为重要,只有在需要提高性能时才考虑优化。 - xaxxon
1
尝试两个版本各数百万次,同时计时调用。在启用和禁用优化的情况下都要进行测试。考虑到返回值优化和复制省略,我怀疑你会发现任何大的差异。 - Some programmer dude
3
在何时进行程序优化:通常情况下,建议在编写程序时专注于可读性、可维护性和正确性,而不是过早地进行优化。但是,当程序的性能成为瓶颈或者无法满足需求时,就需要考虑进行优化。在开始优化之前,应该先进行基准测试和分析,以确定哪些部分需要优化。优化的目标应该是尽可能减少时间和空间复杂度,同时保持代码的可读性和可维护性。 - xaxxon
3
由于复制省略和移动语义的使用,许多情况下通过值传递/返回实际上更有效率。 - MikeMB
7
你的工作涉及编写代码,你刚刚了解到“性能剖析”这个概念了吗?那就去学习如何进行性能剖析吧。这比任何问题的答案都更有帮助。如果你在使用受限硬件上工作,并且没有关于该设备的具体信息,那么这里的任何内容都可能不准确。 - xaxxon
显示剩余11条评论
7个回答

39
首先,考虑到返回对象的可读性更高(在性能上也非常相似)而不是通过引用传递对象,因此对于您的项目来说,返回对象并增加可读性可能更有趣,而不会产生重要的性能差异。 如果您想知道最低成本的方法,那么您需要返回什么:
  1. 如果您需要返回一个简单或基本对象,则两种情况下性能都相似。

  2. 如果对象非常大且复杂,则返回它将需要进行复制,这可能比将其作为引用参数慢,但我认为它将花费更少的内存。

无论如何,您必须考虑编译器进行了许多优化,使得这两种性能非常相似。请参见Copy Elision

2
复制省略呢? - juanchopanza
8
实际上,在x86架构上,并忽略编译器的优化,两者将创建相同的汇编代码,因为大于1或2个寄存器的返回值是通过内存区域传递的,该内存区域由调用者分配并通过隐式指针参数传递给被调用者。 - MikeMB

11

由于一种称为复制省略的优化,大多数情况下应使用返回对象的方式。

然而,根据您的函数预期如何使用,通过引用传递对象可能会更好。

例如,看一下std::getline,它通过引用接受一个std::string。该函数旨在用作循环条件,并持续填充一个std::string,直到达到EOF。使用相同的std::string可以重用std::string的存储空间,在每个循环迭代中大幅减少需要执行的内存分配次数。


10

要理解编译不是一件容易的事情。当编译器编译您的代码时,需要考虑很多因素。

不能简单回答这个问题,因为C++标准没有提供标准ABI(抽象二进制接口),所以每个编译器都可以根据自己的喜好编译代码,并且在每次编译中都可能得到不同的结果。

例如,在某些项目中,C++被编译成Microsoft CLR的托管扩展(C++ / CX)。由于那里的一切都已经引用了堆上的对象,我想没有区别。

对于非托管编译来说,答案也不简单。当我思考“XXX是否比YYY运行更快”时,会有几个问题:

  • 你的对象是否可以默认构造?
  • 你的编译器是否支持返回值优化?
  • 你的对象是否只支持复制语义还是同时支持复制和移动?
  • 该对象是否紧密排列(例如std :: array ),还是它具有指向堆上的东西的指针(例如std :: vector )?

如果给出具体的示例,我的猜测是在MSVC ++和GCC上,通过值返回std :: vector 与通过引用传递它相同,因为r-value优化,将比通过移动返回向量稍快一点(几个纳秒)。在Clang上可能完全不同。

最终,性能分析是唯一的真正答案。


5
一些答案已经提到了这一点,但我想强调一下编辑后的问题。
为了背景,这个问题限制在硬件平台无关的差异上,大部分也是软件。是否存在机器无关的性能差异?
如果这是问题的限制,那么答案是没有答案。C++规范并未规定对象返回或引用传递的性能实现方式,只规定了它们在代码中的语义。
因此,编译器可以自由地将一个优化为与另一个相同的代码,假设这不会对程序员产生可感知的差异。
鉴于此,我认为最好根据情况使用最直观的方法。如果函数确实作为某个任务或查询的结果“返回”对象,请返回它;而如果函数执行的是由外部代码拥有的某个对象的操作,则通过引用传递。
你不能一概而论地说这会影响性能。首先,做任何直观的事情,并查看您的目标系统和编译器如何进行优化。如果在分析之后发现问题,请根据需要进行更改。

4

由于不同平台具有不同的ABIs,因此我们无法完全通用,但我认为我们可以发表一些相当普遍的声明,适用于大多数实现,前提是这些内容主要适用于未嵌入函数。

首先让我们考虑原始类型。在低级别上,参数通过引用传递是使用指针实现的,而原始返回值通常是直接以寄存器传递的。因此,返回值可能会更有效率。在某些体系结构中,对于小型结构也是如此。复制一个足以适应一个或两个寄存器的值非常便宜。

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

总体而言,在原始类型方面,我预计返回值稍微更好,在较大但仍然简单的类型方面,我预计它们相同或更好,除非你的编译器非常不擅长复制省略。

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


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()类型的函数)。

如果您愿意,可以自行测量差异,正如其他人已经提到的那样。对两个版本运行测试代码数百万次,并查看差异。


我想指出,const引用由于引用的隐式状态可能会导致更多的问题。每当您更改引用时,我们必须确保任何其他参数的状态也不会被引用更改。 - CinchBlue
除了 Vermillion 所说的之外,编译器不会将返回副本优化为移动:返回值是基于移动定义的(复制是备选项)。但是它可能会完全省略移动。 - MikeMB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接