哪个更有效率:返回一个值与传递参考?
我目前正在研究如何编写高效的C ++代码,在函数调用的问题上,我想到一个问题。 比较这个伪码function:
not-void function-name () { do-something return value; } int main () { ... arg = function-name(); ... }
与这个否则相同的伪码function:
void function-name (not-void& arg) { do-something arg = value; } int main () { ... function-name(arg); ... }
哪个版本更有效率,哪些方面(时间,内存等)? 如果取决于,那么第一个什么时候会更有效率,第二个什么时候更有效率?
编辑 :对于上下文来说,这个问题仅限于硬件平台无关的差异,对于大多数软件来说也是如此。 有没有机器独立的性能差异?
编辑 :我不明白这是如何重复。 另一个问题是比较通过引用(prev代码)传递的价值(下):
not-void function-name (not-void arg)
这和我的问题不一样。 我的注意力不在于哪一种更好的方法将parameter passing给函数。 我的焦点是从哪个方面更好地将结果传递给外部范围的variables。
首先,考虑到返回一个对象总是比通过引用传递更具可读性(和性能非常相似),所以对于您的项目来说,返回对象并增加可读性而不会有重要的性能差异。 如果你想知道如何有最低的成本,那么你需要回报什么:
-
如果您需要返回一个简单或基本的对象,则在两种情况下的性能都会相似。
-
如果对象是如此庞大而复杂,返回它需要一个副本,它可能比作为引用参数慢,但它会花费更less的内存,我认为。
不pipe怎么样,编译器都会做很多优化,使得两个performance都非常相似。 见复制Elision 。
那么,我们必须明白,汇编不是一件容易的事情。 当编译器编译你的代码的时候有很多考虑。
我们不能简单地回答这个问题,因为C ++标准没有提供标准的ABI(抽象二进制接口),所以每个编译器都可以编译代码,不pipe它喜欢什么,你可以在每个编译中得到不同的结果。
例如,在某些项目上,C ++被编译为Microsoft CLR(C ++ / CX)的托pipe扩展。 因为所有的东西都已经有了一个堆的对象的引用,所以我猜这是没有区别的。
对于不可pipe理的编译来说,答案并不简单。 例如,当我想到“XXX会比YYY跑得快吗?”时,会想到几个问题:
- 你反对deafult-constructible?
- 你的编译器是否支持返回值优化?
- 你的对象是否支持只复制语义或复制和移动?
- 对象是以连续方式打包的(例如
std::array
)还是指向堆上的东西? (例如std::vector
)?
如果我举个具体的例子,我的猜测是,在MSVC ++和GCC上,通过值返回std::vector
将是通过引用传递它,因为r值优化,并且会稍微快一些(几纳秒)然后通过移动来返回vector。 例如,这可能在Clang上完全不同。
最终,剖析是唯一真正的答案。
在大多数情况下,应该使用返回的对象,因为一个称为复制elision的对手。
但是,根据您的函数如何使用,最好通过引用来传递对象。
例如,看std::getline
,它通过引用接受一个std::string
。 此函数旨在用作循环条件并保持填充std::string
直到到达EOF。 使用相同的std::string
可以在每次循环迭代中重用std::string
的存储空间,大大减less需要执行的内存分配次数。
其中一些答案已经涉及到这一点,但是我想强调一下编辑
对于上下文来说,这个问题仅限于硬件平台无关的差异,而大部分软件也是如此。 有没有机器独立的性能差异?
如果这是问题的局限性,答案是没有答案。 c ++规范并没有规定如何在性能方面实现对象的返回或通过引用传递,而只是在代码方面他们都做了什么的语义。
因此,编译器可以自由地将一个代码优化为相同的代码,而另一个代码则不会给程序员带来明显的差异。
鉴于此,我认为最好使用最直观的情况。 如果该函数确实是某个任务或查询的结果“返回”对象,则返回该对象,而如果该函数正在对外部代码所拥有的某个对象执行操作,则通过引用传递。
你不能总结性能,作为一个开始,做任何直观的事情,看你的目标系统和编译器如何优化它。 如果在分析和发现问题之后,根据需要更改它。
这个伪码function:
not-void function-name () { do-something return value; }
当返回的值不需要进一步修改时会更好。 传递的参数只能在function-name
修改。 没有更多的参考需要它。
否则相同的伪码function:
void function-name (not-void& arg) { do-something arg = value; }
如果我们有另一种方法来调节同一个variables的值,我们需要通过任何一个调用来保持对variables所做的更改。
void another-function-name (not-void& arg) { do-something arg = value; }
我们不能100%一般,因为不同的平台有不同的ABI,但是我认为我们可以做出一些相当一般的陈述,这些陈述将适用于大多数实现,但是这些东西大部分适用于未内联的函数。
首先让我们考虑原始types。 在低级别,通过引用传递的参数是使用指针实现的,而基元返回值通常在寄存器中逐字地传递。 所以返回值可能会更好。 在一些架构上,这同样适用于小型结构。 复制一个足够小的值以适应一个或两个寄存器是非常便宜的。
现在让我们考虑更大,但仍然简单(没有默认的构造函数,复制构造函数等)返回值。 通常情况下,较大的返回值是通过传递一个指向返回值所在位置的指针来处理的。 复制elision允许从函数返回的variables,用于返回的临时variables和调用程序中放置结果的variables合并为一个。 所以通过传递的基础和通过引用和返回值是大致相同的。
总体来说,对于原始types,我期望返回值稍微好一点,对于较大但仍然简单的types,我希望它们是相同或更好的,除非您的编译器在复制elision时非常糟糕。
对于使用默认构造函数的types,复制构造函数等,事情变得更加复杂。 如果函数被多次调用,则返回值将强制每次都重新构造对象,而参考参数可能允许重构数据结构而不重构。 另一方面,在调用函数之前,引用参数会强制(可能是不必要的)构造。
性能方面,副本通常比较昂贵,但对于小型对象来说,差异可能会忽略不计。 另外,您的编译器可能会优化返回副本,这相当于传递引用。
我build议不传递非const
引用,除非你有一个很好的理由。 使用返回值(例如tryGet()
sorting的函数)。
如果你想要的话,你可以像其他人所说的那样衡量自己的差异。 为两个版本运行testing代码几百万次,并查看其差异。