按值传递与按引用或按指针传递的性能成本?
Performance cost of passing by value vs. by reference or by pointer?
让我们考虑一个对象 foo
(可能是 int
、double
、自定义 struct
、class
,等等)。我的理解是,通过引用函数传递 foo
(或仅传递指向 foo
的指针)会导致更高的性能,因为我们避免制作本地副本(如果 foo
可能会很昂贵)很大)。
但是,从答案 here 看来,无论指向什么,实际上 64 位系统上的指针都应该有 8 个字节的大小。在我的系统上,float
是 4 个字节。这是否意味着如果 foo
是 float
类型,那么 更有效 只按值传递 foo
而不是给出指向它(假设没有其他约束会使在函数内使用一个比另一个更有效)?
Does that mean that if foo is of type float, then it is more efficient to just pass foo by value?
按值传递浮点数可能更有效。我希望它更有效率——部分原因是你所说的:浮点数小于你描述的系统上的指针。但是除此之外,当你复制指针时,你仍然需要解引用指针来获取函数内的值。指针添加的间接寻址可能会对性能产生重大影响。
效率差异可以忽略不计。特别是,如果可以内联函数并启用优化,则可能不会有任何差异。
您可以通过测量了解在您的情况下按值传递浮点数是否有任何性能提升。您可以使用分析工具来衡量效率。
您可以将指针替换为引用,答案仍然同样适用。
Is there some sort of overhead in using a reference, the way that there is when a pointer must be dereferenced?
是的。引用很可能具有与指针完全相同的性能特征。如果可以使用引用或指针编写语义等效的程序,则两者都可能生成相同的程序集。
如果通过指针传递一个小对象比复制它更快,那么对于相同大小的对象肯定也是如此,您同意吗?指向指针的指针怎么样,大约是指针的大小,对吧? (它的大小完全一样。)哦,但是指针也是对象。因此,如果通过指针传递对象(例如指针)比复制对象(指针)更快,那么将指针传递给指针再传递给指针……传递给指针会比程序更快使用更少的指针仍然比不使用指针的指针更快......也许我们在这里找到了无限的效率来源:)
这取决于您所说的 "cost" 的含义,以及主机系统(硬件、操作系统)在操作方面的属性。
如果您的成本衡量指标是内存使用量,那么成本的计算就很明显了 - 将要复制的内容的大小相加。
如果您的衡量标准是执行速度(或"efficiency"),那么游戏就不同了。借助专用电路(机器寄存器及其使用方式),硬件(以及操作系统和编译器)倾向于针对复制特定大小的事物的操作性能进行优化。
例如,一台机器的架构(机器寄存器、内存架构等)很常见,这会导致 "sweet spot" - 复制某些大小的变量最多 "efficient" ,但复制更大或更小的变量则不然。较大的变量将花费更多的复制成本,因为可能需要对较小的块进行多次复制。较小的也可能花费更多,因为编译器需要将较小的值复制到较大的变量(或寄存器)中,对其进行操作,然后再将值复制回来。
浮点数的例子包括一些原生支持双精度浮点数(在 C++ 中又名 double
)的 cray 超级计算机,并且模拟所有单精度运算(在 C++ 中又名 float
)在软件中。一些较旧的 32 位 x86 CPU 也在内部使用 32 位整数,并且由于转换 to/from 32 位,对 16 位整数的操作需要更多的时钟周期(对于更现代的 32 位或64 位 x86 处理器,因为它们允许复制 16 位整数 to/from 32 位寄存器,并对其进行操作,这样的惩罚更少。
按值复制非常大的结构比创建和复制其地址的效率要低,这有点不费吹灰之力。但是,由于上述因素,"best to copy something of that size by value" 和 "best to pass its address" 之间的交叉点不太清楚。
指针和引用往往以类似的方式实现(例如,通过引用传递可以以与传递指针相同的方式实现),但这并不能保证。
唯一确定的方法是测量它。并意识到测量值会因系统而异。
您必须测试性能绝对关键的任何给定场景,但在尝试强制编译器以特定方式生成代码时要非常小心。
允许编译器的优化器以它选择的任何方式重写您的代码,只要最终结果可证明是相同的,这可以带来一些非常好的优化。
考虑到按值传递浮点数需要复制浮点数,但在适当的条件下,按引用传递浮点数可以允许将原始浮点数存储在 CPU 浮点寄存器中,并且将该寄存器视为函数的 "reference" 参数。相比之下,如果你传递一个副本,编译器必须找到一个地方来存储副本以保存寄存器的内容,或者更糟的是,它可能根本无法使用寄存器,因为需要保留原件(这在递归函数中尤其如此!)。
如果您将引用传递给可以内联的函数,这种区别也很重要,因为编译器不必保证复制的参数不能修改原始参数,因此引用可以减少内联的成本.
一种语言越能让您专注于描述您想要完成的工作而不是您希望它如何完成,编译器就越能够找到创造性的方法来为您完成艰苦的工作。尤其是在 C++ 中,通常最好不要担心性能,而是专注于尽可能清楚和简单地描述您想要的内容。通过尝试描述您希望如何完成工作,您将经常阻止编译器为您优化代码。
有一件事没人提到。
有一个名为 IPA SRA 的特定 GCC 优化,它会自动将 "pass by reference" 替换为 "pass by value":https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html (-fipa-sra)
这很可能是针对没有 non-default 复制语义并且可以放入 cpu 寄存器的标量类型(例如 int、double 等)完成的。
这使得
void(const int &f)
可能同样快(并且 space 优化)
void(int f)
因此启用此优化后,对小型类型使用引用应该与按值传递它们一样快。
另一方面,按值传递(例如)std::string 无法优化到 by-reference 速度,因为涉及自定义复制语义。
据我了解,对所有内容使用按引用传递绝不会比手动选择按值传递和按引用传递的内容慢。
这对模板特别有用:
template<class T>
void f(const T&)
{
// Something
}
总是最优的
如果您希望优化执行时间以避免随机访问,请始终优先考虑通过引用传递而不是指针传递。对于按引用传递与按值传递,GCC 优化您的代码,以便不需要更改的小变量将按值传递。
让我们考虑一个对象 foo
(可能是 int
、double
、自定义 struct
、class
,等等)。我的理解是,通过引用函数传递 foo
(或仅传递指向 foo
的指针)会导致更高的性能,因为我们避免制作本地副本(如果 foo
可能会很昂贵)很大)。
但是,从答案 here 看来,无论指向什么,实际上 64 位系统上的指针都应该有 8 个字节的大小。在我的系统上,float
是 4 个字节。这是否意味着如果 foo
是 float
类型,那么 更有效 只按值传递 foo
而不是给出指向它(假设没有其他约束会使在函数内使用一个比另一个更有效)?
Does that mean that if foo is of type float, then it is more efficient to just pass foo by value?
按值传递浮点数可能更有效。我希望它更有效率——部分原因是你所说的:浮点数小于你描述的系统上的指针。但是除此之外,当你复制指针时,你仍然需要解引用指针来获取函数内的值。指针添加的间接寻址可能会对性能产生重大影响。
效率差异可以忽略不计。特别是,如果可以内联函数并启用优化,则可能不会有任何差异。
您可以通过测量了解在您的情况下按值传递浮点数是否有任何性能提升。您可以使用分析工具来衡量效率。
您可以将指针替换为引用,答案仍然同样适用。
Is there some sort of overhead in using a reference, the way that there is when a pointer must be dereferenced?
是的。引用很可能具有与指针完全相同的性能特征。如果可以使用引用或指针编写语义等效的程序,则两者都可能生成相同的程序集。
如果通过指针传递一个小对象比复制它更快,那么对于相同大小的对象肯定也是如此,您同意吗?指向指针的指针怎么样,大约是指针的大小,对吧? (它的大小完全一样。)哦,但是指针也是对象。因此,如果通过指针传递对象(例如指针)比复制对象(指针)更快,那么将指针传递给指针再传递给指针……传递给指针会比程序更快使用更少的指针仍然比不使用指针的指针更快......也许我们在这里找到了无限的效率来源:)
这取决于您所说的 "cost" 的含义,以及主机系统(硬件、操作系统)在操作方面的属性。
如果您的成本衡量指标是内存使用量,那么成本的计算就很明显了 - 将要复制的内容的大小相加。
如果您的衡量标准是执行速度(或"efficiency"),那么游戏就不同了。借助专用电路(机器寄存器及其使用方式),硬件(以及操作系统和编译器)倾向于针对复制特定大小的事物的操作性能进行优化。
例如,一台机器的架构(机器寄存器、内存架构等)很常见,这会导致 "sweet spot" - 复制某些大小的变量最多 "efficient" ,但复制更大或更小的变量则不然。较大的变量将花费更多的复制成本,因为可能需要对较小的块进行多次复制。较小的也可能花费更多,因为编译器需要将较小的值复制到较大的变量(或寄存器)中,对其进行操作,然后再将值复制回来。
浮点数的例子包括一些原生支持双精度浮点数(在 C++ 中又名 double
)的 cray 超级计算机,并且模拟所有单精度运算(在 C++ 中又名 float
)在软件中。一些较旧的 32 位 x86 CPU 也在内部使用 32 位整数,并且由于转换 to/from 32 位,对 16 位整数的操作需要更多的时钟周期(对于更现代的 32 位或64 位 x86 处理器,因为它们允许复制 16 位整数 to/from 32 位寄存器,并对其进行操作,这样的惩罚更少。
按值复制非常大的结构比创建和复制其地址的效率要低,这有点不费吹灰之力。但是,由于上述因素,"best to copy something of that size by value" 和 "best to pass its address" 之间的交叉点不太清楚。
指针和引用往往以类似的方式实现(例如,通过引用传递可以以与传递指针相同的方式实现),但这并不能保证。
唯一确定的方法是测量它。并意识到测量值会因系统而异。
您必须测试性能绝对关键的任何给定场景,但在尝试强制编译器以特定方式生成代码时要非常小心。
允许编译器的优化器以它选择的任何方式重写您的代码,只要最终结果可证明是相同的,这可以带来一些非常好的优化。
考虑到按值传递浮点数需要复制浮点数,但在适当的条件下,按引用传递浮点数可以允许将原始浮点数存储在 CPU 浮点寄存器中,并且将该寄存器视为函数的 "reference" 参数。相比之下,如果你传递一个副本,编译器必须找到一个地方来存储副本以保存寄存器的内容,或者更糟的是,它可能根本无法使用寄存器,因为需要保留原件(这在递归函数中尤其如此!)。
如果您将引用传递给可以内联的函数,这种区别也很重要,因为编译器不必保证复制的参数不能修改原始参数,因此引用可以减少内联的成本.
一种语言越能让您专注于描述您想要完成的工作而不是您希望它如何完成,编译器就越能够找到创造性的方法来为您完成艰苦的工作。尤其是在 C++ 中,通常最好不要担心性能,而是专注于尽可能清楚和简单地描述您想要的内容。通过尝试描述您希望如何完成工作,您将经常阻止编译器为您优化代码。
有一件事没人提到。
有一个名为 IPA SRA 的特定 GCC 优化,它会自动将 "pass by reference" 替换为 "pass by value":https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html (-fipa-sra)
这很可能是针对没有 non-default 复制语义并且可以放入 cpu 寄存器的标量类型(例如 int、double 等)完成的。
这使得
void(const int &f)
可能同样快(并且 space 优化)
void(int f)
因此启用此优化后,对小型类型使用引用应该与按值传递它们一样快。
另一方面,按值传递(例如)std::string 无法优化到 by-reference 速度,因为涉及自定义复制语义。
据我了解,对所有内容使用按引用传递绝不会比手动选择按值传递和按引用传递的内容慢。
这对模板特别有用:
template<class T>
void f(const T&)
{
// Something
}
总是最优的
如果您希望优化执行时间以避免随机访问,请始终优先考虑通过引用传递而不是指针传递。对于按引用传递与按值传递,GCC 优化您的代码,以便不需要更改的小变量将按值传递。