【问题标题】:Performance cost of passing by value vs. by reference or by pointer?按值传递与按引用或指针传递的性能成本?
【发布时间】:2017-03-04 07:08:19
【问题描述】:

让我们考虑一个对象foo(可能是int、double、自定义struct、class,等等)。我的理解是,通过引用函数来传递 foo(或仅传递指向 foo 的指针)会导致更高的性能,因为我们避免制作本地副本(如果 foo 很大,这可能会很昂贵)。

但是,从答案here 看来,实际上可以预期 64 位系统上的指针大小为 8 字节,无论指向什么。在我的系统上,float 是 4 个字节。这是否意味着如果foo 是float 类型,那么仅通过值传递foo 而不是提供指向它的指针会更有效(假设没有其他约束会使在函数内部使用一个比另一个更有效)?

【问题讨论】:

  • 你应该测量它。被引用/复制的东西的大小并不是唯一起作用的东西。
  • 简而言之:通过值传递本地类型(int、float、double)几乎总是比通过引用更有效。不仅因为在大多数情况下,指针比原生数据类型更大或一样大,还因为优化器优化引用参数比优化值参数要困难得多。
  • 这是无法回答的。 c++ 标准没有说明这个成本。不同的编译器有不同的优化。其中任何一个都可能是免费的。

标签: c++ pointers pass-by-reference pass-by-value pass-by-pointer


【解决方案1】:

这取决于您所说的“成本”是什么意思,以及主机系统(硬件、操作系统)相对于操作的属性。

如果您的成本衡量标准是内存使用量,那么成本的计算是显而易见的 - 将要复制的任何内容的大小相加。

如果您的衡量标准是执行速度(或“效率”),那么游戏就不同了。借助专用电路(机器寄存器及其使用方式),硬件(以及操作系统和编译器)往往会针对复制特定大小的事物的操作性能进行优化。

例如,对于具有导致“最佳位置”的架构(机器寄存器、内存架构等)的机器来说,这是很常见的——复制某种大小的变量是最“有效的”,但复制更大的 OR较小的变量则更少。较大的变量将花费更多的复制成本,因为可能需要对较小的块进行多次复制。较小的也可能成本更高,因为编译器需要将较小的值复制到较大的变量(或寄存器)中,对其进行操作,然后将值复制回来。

浮点示例包括一些 cray 超级计算机,它们本机支持双精度浮点(在 C++ 中又称为 double),并且所有单精度运算(在 C++ 中又称为 float)都在软件中模拟。一些较旧的 32 位 x86 CPU 在内部也使用 32 位整数,并且由于与 32 位之间的转换,对 16 位整数的操作需要更多的时钟周期(这不适用于更现代的 32 位或 64-位 x86 处理器,因为它们允许将 16 位整数复制到 32 位寄存器或从 32 位寄存器复制,并对其进行操作,这样的惩罚更少)。

按值复制一个非常大的结构将比创建和复制其地址效率低,这有点不费吹灰之力。但是,由于上述因素,“最好按值复制该大小的东西”和“最好传递其地址”之间的交叉点不太清楚。

指针和引用倾向于以类似的方式实现(例如,按引用传递可以以与传递指针相同的方式实现),但这并不能保证。

唯一确定的方法是测量它。并意识到测量值会因系统而异。

【讨论】:

  • 你知道一个实际的架构示例吗,传递较小的类型(例如 char)比传递较大的类型(例如 int 或指针)更昂贵?
  • 是的,好的,添加了几个示例。
  • 谢谢,但是这些例子中的任何一个与指针/引用传递与值传递的问题有关吗?毕竟,这与传递浮点数与传递双精度数无关。
  • “操作”一词包括但不限于复制值。关键是传递较小的东西不一定比传递更大的东西更“有效”。这通常是传递指针(或引用)与值的效率参数类型。
【解决方案2】:

有一件事没人提到。

有一种称为 IPA SRA 的 GCC 优化,它自动将“按引用传递”替换为“按值传递”:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html (-fipa-sra)

这很可能适用于标量类型(例如 int、double 等),它们没有非默认复制语义并且可以放入 cpu 寄存器中。

这让

void(const int &f)

可能一样快(并且优化了空间)

void(int f)

因此启用此优化后,使用小类型的引用应该与通过值传递它们一样快。

另一方面,按值传递(例如)std::string 无法优化到按引用速度,因为涉及到自定义复制语义。

据我了解,对所有内容使用按引用传递永远不会比手动选择按值传递的内容和按引用传递的内容要慢。

这对于模板特别有用:

template<class T>
void f(const T&)
{
    // Something
}

总是最优的

【讨论】:

  • 此优化是否适用于转发引用?如果是这样,不是说template&lt;typename T&gt; void f(T&amp;&amp;) { ... } 在一般情况下更理想吗?
  • 由于没有标记 GCC,是否有其他编译器(最重要的是 Clang 和 Visual C++)的等效优化?
【解决方案3】:

您必须测试性能绝对关键的任何给定场景,但在尝试强制编译器以特定方式生成代码时要非常小心。

允许编译器的优化器以它选择的任何方式重写您的代码,只要最终结果可证明是相同的,这可以导致一些非常好的优化。

考虑按值传递浮点数需要制作浮点数的副本,但在正确的条件下,通过引用传递浮点数可以允许将原始浮点数存储在 CPU 浮点寄存器中,并将该寄存器视为“参考”参数的功能。相比之下,如果你传递一个副本,编译器必须找到一个存储副本的地方才能保存寄存器的内容,或者更糟糕的是,它可能根本无法使用寄存器,因为需要保留原件(在递归函数中尤其如此!)。

如果您将引用传递给可以内联的函数,这种差异也很重要,其中引用可能会降低内联的成本,因为编译器不必保证复制的参数不能修改原始参数。

一种语言越能让您专注于描述您想要完成的工作,而不是您希望它如何完成,编译器就越能找到创造性的方式来为您完成艰巨的工作。特别是在 C++ 中,通常最好不要担心性能,而是专注于尽可能清晰和简单地描述您想要的内容。通过尝试描述您希望如何完成工作,您通常会阻止编译器为您优化代码。

【讨论】:

  • 通常情况正好相反:当您通过引用/指针传递参数时,然后 - 在实践中 - 该参数始终必须写入内存,而通过值传递有时允许保留寄存器中的数据。
  • @MikeMB - 在我上面介绍的场景中不是这种情况,原始副本存储在寄存器中;按值传递需要不同的副本以保留原始内容,因此要么必须使用额外的寄存器(如果可用),要么由于寄存器太少而必须将整个寄存器优化展开到内存中。相比之下,通过引用传递可以允许编译器在两段代码之间共享相同的寄存器(特别是如果函数是内联的)。我不认为这是一种常见的情况,但肯定有可能。
  • 假设没有函数内联发生。然后通过引用方式传递 - 根据我知道的调用约定 - 指向要传递给函数的原始内存位置 HAS 的指针,并且需要将值实际存储在内存中,因为指针不能指向寄存器。通过值传递时,您可能必须将值从一个寄存器复制到另一个寄存器(如果在函数调用后未使用该值,则不需要),但您不必将其存储在内存中。
【解决方案4】:

这是否意味着如果 foo 是 float 类型,那么只通过值传递 foo 会更有效?

按值传递浮点数可能更有效。我希望它更有效 - 部分原因是您所说的:浮点数小于您描述的系统上的指针。但除此之外,在复制指针时,仍然需要对指针进行解引用才能获取函数内的值。指针添加的间接性可能会对性能产生重大影响。

效率差异可以忽略不计。特别是,如果可以内联函数并启用优化,则可能不会有任何区别。

您可以通过测量来了解在您的情况下通过值传递浮点数是否有任何性能提升。您可以使用分析工具来衡量效率。

您可以用引用替换指针,答案仍然同样适用。

在使用引用时是否存在某种开销,就像必须取消引用指针时那样?

是的。引用很可能具有与指针完全相同的性能特征。如果可以使用引用或指针编写语义等效的程序,那么两者可能都会生成相同的程序集。


如果通过指针传递一个小对象比复制它更快,那么对于相同大小的对象肯定会是这样,你不同意吗?指向指针的指针怎么样,大约是指针的大小,对吧? (大小完全相同。)哦,但指针也是对象。因此,如果通过指针传递对象(例如指针)比复制对象(指针)更快,那么将指向指针的指针传递给指向指针的指针...指向指针将比程序更快使用更少的指针仍然比不使用指针的更快......也许我们在这里找到了无限的效率来源:)

【讨论】:

  • 在使用引用时是否存在某种开销,就像必须取消引用指针时那样?
【解决方案5】:

如果您想要优化执行时间以避免随机访问,请始终优先考虑按引用传递而不是指针。对于按引用传递与按值传递,GCC 会优化您的代码,以便不需要更改的小变量将按值传递。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-03-24
    • 1970-01-01
    • 2012-01-16
    • 2012-10-14
    • 1970-01-01
    • 2012-01-24
    相关资源
    最近更新 更多