按值传递与按引用或指针传递的性能成本?

Performance cost of passing by value vs. by reference or by pointer?

提问人:space_voyager 提问时间:10/22/2016 最后编辑:Communityspace_voyager 更新时间:2/17/2023 访问量:57421

问:

让我们考虑一个对象(可以是 , a , a custom , a , whatever)。我的理解是,通过引用函数(或只是传递指针)传递会导致更高的性能,因为我们避免了制作本地副本(如果很大,这可能会很昂贵)。foointdoublestructclassfoofoofoo

但是,从此处的答案来看,无论指向什么,实际上 64 位系统上的指针的大小似乎都为 8 字节。在我的系统上,a 是 4 个字节。这是否意味着如果 是 类型,那么只传递值而不是给它一个指针更有效(假设没有其他约束会使在函数中使用一个比另一个更有效)?floatfoofloatfoo

C++ 指针 引用 按值传递

评论

9赞 juanchopanza 10/22/2016
你应该测量它。被引用/复制的事物的大小并不是唯一起作用的因素。
0赞 Humam Helfawi 10/22/2016
stackoverflow.com/questions/21605579/......
5赞 MikeMB 10/22/2016
简而言之:按值传递本机类型(int、float、double)几乎总是比按引用传递更有效。这不仅是因为指针(在大多数情况下)比本机数据类型更大或一样大,还因为优化器优化引用参数比值参数更难。
1赞 Captain Giraffe 10/22/2016
这是无法回答的。c++ 标准没有说明这个成本。不同的编译器有不同的优化。这些中的任何一个都可能是免费的。

答:

7赞 eerorika 10/22/2016 #1

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

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

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

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

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

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

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


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

评论

0赞 space_voyager 10/22/2016
使用引用时是否存在某种开销,就像必须取消引用指针时一样?
41赞 Peter 10/22/2016 #2

这取决于您所说的“成本”的含义,以及主机系统(硬件、操作系统)与操作相关的属性。

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

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

例如,对于一台机器来说,通常有一个架构(机器寄存器、内存架构等),这会导致一个“甜蜜点”——复制一定大小的变量是最“有效”的,但复制更大或更小的变量就不那么“有效”。较大的变量复制成本更高,因为可能需要对较小的块进行多个复制。较小的值也可能花费更多,因为编译器需要将较小的值复制到较大的变量(或寄存器)中,对其执行操作,然后将值复制回来。

浮点的例子包括一些 cray 超级计算机,它们本身支持双精度浮点(在 C++ 中又名),并且所有单精度(在 C++ 中)的操作都在软件中模拟。一些较旧的 32 位 x86 CPU 也在内部使用 32 位整数工作,并且由于与 32 位之间的转换,对 16 位整数的操作需要更多的时钟周期(对于更现代的 32 位或 64 位 x86 处理器,情况并非如此,因为它们允许将 16 位整数复制到 32 位寄存器或从 32 位寄存器复制 16 位整数, 并对它们进行手术,减少此类处罚)。doublefloat

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

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

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

评论

4赞 MikeMB 10/22/2016
您是否知道一个架构的实际示例,其中传递较小的类型(例如 char)比传递较大的类型(例如 int 或指针)更昂贵?
0赞 Peter 10/22/2016
是的,好的,添加了几个示例。
0赞 MikeMB 10/22/2016
谢谢,但是这些示例中的任何一个是否与指针/引用传递与值传递的问题相关?毕竟,这不是关于传递浮点数与传递双倍数。
1赞 Peter 10/22/2016
术语“操作”包括但不限于复制值。关键是,传递较小的东西并不一定比传递较大的东西更“有效”。这通常是传递指针(或引用)与值时引用的效率参数类型。
0赞 mschoenebeck 2/17/2023
答案是不正确的:在 64 位系统上,即使是较小的类型也始终与内存中的 64 位对齐。例如,这意味着单个字符不是存储在任意字节地址,而是始终存储在 8 个字节的倍数中。因此,处理较小的类型并不比处理 64 位类型更昂贵。它们在 64 位系统上具有相同的成本。
9赞 Matt Jordan 10/22/2016 #3

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

编译器的优化器可以按照它选择的任何方式重写你的代码,只要最终结果是可证明的,这可能会导致一些非常好的优化。

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

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

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

评论

2赞 MikeMB 10/22/2016
通常情况正好相反:当您通过引用/指针传递参数时,实际上该参数始终必须写入内存,而按值传递它有时允许将数据保留在寄存器中。
0赞 Matt Jordan 11/8/2016
@MikeMB - 在我上面介绍的场景中,情况并非如此,原始副本存储在寄存器中;按值传递需要不同的副本以保留原始寄存器的内容,因此,如果有额外的寄存器(如果可用),则必须使用额外的寄存器,或者由于寄存器太少,必须将整个寄存器优化展开到内存中。相比之下,通过引用传递可以允许编译器在两段代码之间共享相同的寄存器(尤其是在函数内联的情况下)。我不认为这是一种常见的情况,但肯定是可能的。
6赞 MikeMB 11/8/2016
假设没有函数内联发生。然后通过引用传递意味着 - 根据我所知道的调用约定 - 必须将指向原始内存位置的指针传递给函数,并且要求值实际存储在内存中,因为指针不能指向寄存器。按值传递时,您可能必须将 falue 从一个寄存器复制到另一个寄存器(如果函数调用后未使用该值,则不会),但不必将其存储在内存中。
46赞 peku33 3/28/2018 #4

有一件事没有人提到。

有一种称为 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
}

始终是最佳的

评论

0赞 Christopher Mauer 2/16/2021
此优化是否适用于转发引用?如果是这样,那么在一般情况下,这难道不是更理想的选择吗?template<typename T> void f(T&&) { ... }
0赞 Spencer 11/24/2021
由于这没有标记为 GCC,因此是否有针对其他编译器(最重要的是 Clang 和 Visual C++)的等效优化?
0赞 Mikhail 9/6/2022
嗯,我看不出它能 gcc.godbolt.org/z/rz9TEdbzd 从某种意义上说,这有点奇怪,只有当使用此声明的所有代码都使用此优化编译时,这种优化才能正常工作
2赞 Ilyes 6/24/2021 #5

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

评论

0赞 isudfv 4/18/2022
在处理引用时,会进行哪些优化执行?
2赞 mschoenebeck 2/17/2023 #6

不敢相信还没有人提出正确的答案。

在 64 位系统上,传递 8 个字节或 4 个字节的成本完全相同。这样做的原因是数据总线是 64 位宽(即 8 个字节),因此即使您只传递 4 个字节 - 它对机器也没有影响:数据总线是 8 个字节宽。

只有当您要移动超过 64 位时,成本才会增加。所有等于或低于 64 位的时钟周期数相同。