按值传递还是按引用传递标量类型:有关系吗?

8

虽然微优化很愚蠢,可能会导致实践中的许多错误。不管怎样,我见过很多人这样做:

void function( const double& x ) {}

改为:

void function( double x ) {}

因为它被认为是“更有效率的”。假设在程序中,function 被无数次地调用,那么这种“优化”真的有意义吗?

@suspectus 当然,没错。不过那不是我的问题 :) - Jonathan H
1
@suspectus 没有应用 const - user2249683
相关:https://dev59.com/MVzUa4cB1Zd3GeqP6cML#7958278 - Mgetz
@Mgetz 你说得完全正确,抱歉!让我读一下这篇帖子,然后我可能会关闭我的问题。再次抱歉,我没有找到那个.. - Jonathan H
“数百万次;我个人确信这没有任何影响…”- 你为什么会相信这种说法呢?在许多平台上,它肯定会产生影响;例如一个 4 字节指针比 8 字节的double更小。问题的关键是:这种差异实际上是否会对我的(或你的)代码产生影响?要回答这个问题,需要提供一个具体的例子。 - Ed S.
显示剩余5条评论
4个回答

12
长话短说,不需要,尤其是在大多数现代平台上,标量甚至浮点类型都通过寄存器传递。一般的经验法则是使用128字节作为分界线,超过这个大小应使用按引用传递而非按值传递。
由于数据已经存储在寄存器中,因此要求处理器从缓存/内存中获取数据实际上会减慢速度。这可能会对性能造成巨大影响,具体取决于数据所在的缓存行是否无效。
最终取决于平台ABI和调用约定是什么。当编译器进行优化时,大多数现代编译器甚至会使用寄存器来传递数据结构(例如,一个两个short变量的结构体等)。

@AntonTykhyy 这真的是一个判断问题,个人而言我会保持它更小,但这只是我的看法。不过我在多个地方都见过128字节的情况。 - Mgetz
@Basilevs 人们常常忘记他们正在使用多任务系统...即使是应该很近的堆栈,也可能因为中断或时钟结束而远得多。 - Mgetz

3
在这种情况下,通过引用传递肯定不是更高效的。请注意,使用“const”限定符限定的引用并不意味着所引用的对象不能改变。此外,这也不意味着函数本身不能改变它(如果裁判不是常数,则函数可以合法地使用“const_cast”来摆脱该“const”)。考虑到这一点,很明显,通过引用传递强制编译器考虑可能的别名问题,在一般情况下,这会导致[明显]不太高效的代码在按引用传递的情况下生成。
为了排除可能出现的别名问题,需要在后者版本中开始进行以下操作:
void function( const double& x ) {
  double non_aliased_x = x;
  // ... and use `non_aliased_x` from now on
  ...
}

但这将与首次通过引用传递的推荐理由相违背。

另一种解决别名问题的方法是使用某种类似于 C99 的 restrict 限定符。

void function( const double& restrict x ) {

但是,即使在这种情况下,按引用传递的缺点可能仍然会超过优点,正如其他答案所解释的那样。


2
在后面的例子中,你可以节省4B的堆栈复制开销。存储double类型需要8B,而存储指针(在32位环境下)或引用只需要4B。引用本质上是一个带有编译器支持的指针。在64位环境下,指针和引用都需要64b=8B,因此无法节省空间。请注意,保留了HTML标签。

3
反过来说,通过引用传递可能会强制将存储在寄存器或FPU堆栈中的double复制到CPU堆栈中。得失相当。 - Steve Jessop

2
除非函数被内联,且根据调用约定(以下假设使用基于堆栈的参数传递,现代调用约定仅在函数具有过多参数时使用),否则在参数传递和使用方面存在两个不同之处:
  • double:该(可能)8字节大小的值被写入堆栈并由函数原样读取。
  • double &double *:该值位于内存中的某个位置(可能“靠近”当前堆栈指针,例如如果它是局部变量,但也可能在远离的地方)。在堆栈上存储一个(可能)48字节大的指针地址(分别为32位或64位系统),函数需要取消引用地址才能读取该值。这还需要该值位于可寻址的内存中,而寄存器则不是。
这意味着,使用引用传递参数所需的堆栈空间可能会稍微少一些。这不仅减少了内存需求,还提高了堆栈顶部字节的缓存效率。当使用引用时,解引用会增加一些额外的工作。
总之,对于大型类型(比如当 sizeof(T) > 32 或者可能更大时),请使用引用。当堆栈大小和热度起到非常重要的作用时,甚至在 sizeof(T) > sizeof(T*) 的情况下也是如此。
*) 如果不是这种情况,请参阅此评论和SOReader的答案。

2
当然,这都取决于特定平台的ABI - 完全有可能double和指针都不是通过堆栈传递,而是通过寄存器传递,这种情况下差异要小得多... - twalberg
@leemes 如果你看一下现代ABIs的集合,你可能会感到惊讶。几乎所有的ABI都会使用栈来传递"大量"参数,但如果只有一个或两个参数,则通常会在寄存器中传递-至少在桌面/服务器类型的系统上;嵌入式ABIs在这方面可能会非常不同,我猜... - twalberg
@twalberg,你能给我指点一些相关文档吗?(我对GCC是如何处理这个的特别感兴趣,但我找不到比维基百科更多的信息,维基百科上说"函数参数按照相反的顺序被压入栈中")... - leemes
@leemes:一般来说,写下long的大小(以及其他类似的细节)的人也会写下调用约定。它们都是ABI的一部分,为了使两个二进制文件链接兼容,它们必须同时使用相同的ABI。如果在单个平台上使用多个调用约定(例如在Win32中),那么您可以使用调用约定装饰您的函数,这就像“C”链接一样成为函数签名的一部分。 - Steve Jessop
1
@leemes:对于Win32来说是正确的,但64位Windows代码只使用一种调用约定,即fastcall,它使用最多4个寄存器传递参数。当GCC编译不同目标时,使用任意数量的不同调用约定。 - Steve Jessop
显示剩余6条评论

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