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


76

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

1
在计时通话的同时,尝试两个版本数百万次。既可以启用优化,也可以不启用优化。考虑到返回值优化和复制删除,我怀疑您是否会发现任何较大的差异。
一些程序员花花了


3
@Pedro:由于复制省略和移动语义,在很多情况下,按值传递/返回实际上更有效。
MikeMB

7
您的工作涉及编写代码,而您刚刚学会了性能分析?快去学习如何进行简介。这将比这个问题为您提供更多帮助。而且,如果您使用的硬件受限制,那么如果没有该设备的特定信息,这里的信息将是不正确的。
xaxxon 2015年

Answers:


27

首先,要考虑到返回对象总是比通过引用传递对象更具可读性(并且在性能上非常相似),因此对于您的项目来说,返回对象并提高可读性而又不存在重大的性能差异可能会更有趣。 。如果您想知道如何以最低的价格买东西,那您需要返回什么:

  1. 如果需要返回一个简单或基本对象,则两种情况下的性能都将相似。

  2. 如果对象太大而又复杂,则返回该对象将需要一个副本,它可能比将其作为引用参数要慢,但我认为它将花费更少的内存。

无论如何,您都必须考虑编译器进行了很多优化,这使得两种性能都非常相似。请参阅复制清除


2
复制省略呢?
juanchopanza 2015年

8
实际上,在x86上,并且忽略编译器优化,两者都将创建相同的汇编代码,因为返回值大于1或2?寄存器通过存储区传递,该存储区由调用方分配并通过隐式指针参数传递给被调用方。
MikeMB

9

好吧,必须理解编译不是一件容易的事。编译器编译您的代码时要考虑很多因素。

一个人不能简单地回答这个问题,因为C ++标准没有提供标准的ABI(抽象二进制接口),因此每个编译器都可以随意编译代码,并且每次编译都可以得到不同的结果。

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

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

  • 您反对违规吗?
  • 您的编译器是否支持返回值优化?
  • 您的对象支持仅复制语义还是同时复制和移动?
  • 对象是以连续方式包装的(例如std::array)还是在堆上具有指向某物的指针?(例如std::vector)?

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

最终,分析是这里唯一的真实答案。


8

在大多数情况下,应使用返回对象的方法,这是因为进行了称为复制删除的优化。

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

再看std::getline例如,需要一个std::string参照。该功能旨在用作循环条件,并一直填充std::string到达到EOF为止。使用相同std::string的存储空间,std::string可以在每次循环迭代中重新使用的存储空间,从而大大减少了需要执行的内存分配数量。


5

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

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

如果这是问题的极限,那么答案是没有答案。c ++规范没有规定如何在性能上实现对象的返回或按引用传递,而仅规定了两者在代码方面的语义。

因此,编译器可以自由地将一个代码优化为与另一个代码相同的代码,前提是这不会对程序员产生明显的影响。

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

您不能对此概括性能。首先,请进行直观的操作,并查看目标系统和编译器对其进行优化的程度。如果在分析后发现问题,请根据需要进行更改。


4

我们不能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()排序函数)。

正如其他人已经说过的,如果您愿意,您可以衡量自己的差异。对这两个版本运行测试代码数百万次,并观察两者之间的区别。


我想指出,由于引用的隐式状态, const引用将导致更多的问题。无论何时更改引用,我们都必须确保该引用也不会更改任何其他参数的状态。
CinchBlue

除了Vermillion所说的之外,编译器不会将return副本优化为move:return是根据move定义的(副本是回退)。但是,这可能会完全抵消这一举动。
MikeMB
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.