按值传递比按常量引用传递更快的经验法则?

Rule of thumb for when passing by value is faster than passing by const reference?

提问人:gexicide 提问时间:10/16/2014 最后编辑:Communitygexicide 更新时间:5/17/2016 访问量:6559

问:

假设我有一个函数,它接受一个类型的参数。 它不会改变它,所以我可以选择通过常量引用或值传递它:Tconst T&T

void foo(T t){ ... }
void foo(const T& t){ ... }

有没有一条经验法则,在通过常量引用变得比按值传递更便宜之前应该变得多大?例如,假设我知道.我应该使用 const 引用还是值?Tsizeof(T) == 24

我假设 的复制构造函数是微不足道的。否则,问题的答案当然取决于复制构造函数的复杂性。T

我已经寻找过类似的问题,并偶然发现了这个问题:

模板按值传递或常量引用或...?

然而,公认的答案(https://stackoverflow.com/a/4876937/1408611)没有说明任何细节,它只是说:

如果期望 T 始终是数值类型或非常 复制成本低廉,然后您可以按值获取参数。

因此,它并没有解决我的问题,而是重新表述了它:一个类型必须有多小才能“复制成本非常低”?

C++ C++11 按引用传递

评论

7赞 10/16/2014
@TheOne 一点也不。您必须考虑加载通过您获取的指针使用的参数部分的成本。这很快就会变得混乱,所以最好的方法是尝试一下。但是,例如,如果您的唯一参数是两个整数的 POD 对,则按值传递可能(取决于 ABI)只使用两个寄存器,而传递指针会浪费一个可用的寄存器,并且需要函数内部的其他指令。
2赞 David Rodríguez - dribeas 10/16/2014
sizeof(std::vector<int>)在某些 32 位实现中为 12,在某些 64 位实现中为 24,但您不想按值传递它,因为内容可能远不止于此,即使情况并非如此,复制构造也需要为任何非空向量分配内存......另一方面,这很大程度上取决于你所针对的平台,你应该查看 ABI 来了解函数调用是如何实现的。
4赞 Angew is no longer proud of SO 10/16/2014
相关阅读:想要速度吗?按值传递。当然,还可以在谷歌上搜索标题,以阅读该文章的所有支持点和对立点。
1赞 10/16/2014
@Angew 一篇好文章,但它专门讨论了你需要副本的情况(因为你要改变它)。
2赞 gexicide 10/16/2014
@DavidRodríguez-dribeas:我指定我假设一个普通的复制构造函数。 当然,这将是一个具有非常不起眼的复制构造函数的示例,因此除非我真的需要副本,否则我什至不会考虑复制一个。std::vectorvector

答:

13赞 Drax 10/16/2014 #1

在我看来,最合适的经验法则是在以下情况下通过引用:

sizeof(T) >= sizeof(T*)

这背后的想法是,当你通过引用时,最坏的情况是你的编译器可能会使用指针来实现这一点。

当然,这没有考虑到复制构造函数和移动语义的复杂性,以及围绕对象生命周期可以创建的所有地狱。

此外,如果你不关心微优化,你可以通过常量引用传递所有内容,在大多数机器上,指针是 4 或 8 个字节,很少有类型比这更小,即使在这种情况下,你也会丢失一些(小于 8)字节的复制操作和一些间接操作,这在现代世界中很可能不会成为你的瓶颈:)

评论

0赞 Basilevs 10/16/2014
我想知道缓存位置是否会以某种方式影响这里的性能?
15赞 10/16/2014
我不认为这条规则在现实中有任何依据。您必须考虑加载通过您获取的指针使用的参数部分的成本。关于这个问题的推理很快就会变得混乱,所以最好的方法是尝试一下。例如,如果唯一的参数是一对两个整数,则按值传递可能(取决于 ABI)只使用两个寄存器,而传递指针会浪费一个可用的寄存器,并且需要在函数内部执行其他指令。我会谨慎行事,即使用指针大小的小倍数。
0赞 Drax 10/16/2014
@Basilevs可能,但在没有任何衡量标准的情况下,我们无法真正讨论这一点,从直觉(这显然是一种关于性能:)的糟糕推理方式),我会说它不是“那么”大,但再次应该检查它
2赞 Drax 10/16/2014
@delnan 好吧,出于某种原因,我不想用“问你的直觉”^^来回答经验法则问题。我不确定填充如何使此规则实现依赖于此规则实现,但即使是这种情况,我仍然认为这是最合适的。
1赞 10/16/2014
@Drax 好点子。我完善了我的论点并将其作为答案发布。我希望它比现在“问你的直觉”更有成效。
28赞 user395760 10/16/2014 #2

如果您有理由怀疑有值得的性能提升,请根据经验和衡量法则将其剔除。您引用的建议的目的是,您不会无缘无故地复制大量数据,但也不要通过将所有内容作为参考来危及优化。如果某些东西介于“复制成本明显低”和“复制成本明显昂贵”之间,那么您可以负担得起任何一种选择。如果你必须把决定权从你身上拿走,那就掷硬币。

如果一个类型没有时髦的复制构造函数并且它很小,那么复制它的成本很低。对于“小”来说,没有最优的硬性数字,即使是在每个平台上也是如此,因为它在很大程度上取决于调用代码和函数本身。只要凭直觉就行了。一、二、三个字都很小。十个,谁知道呢。一个 4x4 矩阵并不小。sizeof

评论

0赞 Karthik T 10/16/2014
还取决于调用此函数的频率。测量的另一个原因。
4赞 quant 12/4/2014
如果一个类型没有时髦的复制构造函数并且它的大小很小,那么复制它的成本很低。- 这可能会产生误导。这里需要注意的是,一个类可能有一个“时髦的复制构造函数”,而程序员却不知道它,例如。这样的类可以有一个小的、没有显式的复制构造函数,但复制起来仍然可能非常昂贵。struct A { std::list<double> x; };sizeof
4赞 Escualo 10/16/2014 #3

我相信我会尽可能地选择按值传递(即:当语义要求我不需要实际对象来工作时)。我相信编译器会执行适当的移动和复制省略。

在我的代码在语义上正确之后,我会对其进行分析,看看我是否正在制作任何不必要的副本;我会相应地修改这些内容。

我相信这种方法将帮助我专注于软件中最重要的部分:正确性。而且我不会妨碍编译器---干扰;inhibit---执行优化(我知道我无法击败它)。

话虽如此,名义上的引用是作为指针实现的。因此,在真空中,在不考虑语义、复制省略、移动语义和类似的东西的情况下,通过指针/引用传递任何大小大于指针的东西会更“有效”。

评论

1赞 10/16/2014
正如我之前在多条评论中解释的那样,不,大于指针并不意味着通过引用传递更有效率,即使在真空中也是如此。
14赞 gnasher729 10/16/2014 #4

传递值而不是 const 引用的优点是编译器知道该值不会更改。“const int&x”并不意味着该值不能更改;这仅意味着不允许使用标识符 x 更改代码(没有编译器会注意到的一些强制转换)。举这个可怕但完全合法的例子:

static int someValue;

void g (int i)
{
    --someValue;
}

void f (const int& x)
{
    for (int i = 0; i < x; ++i)
        g (i);
}

int main (void)
{
    someValue = 100;
    f (someValue);
    return 0;
}

在函数 f 中,x 实际上不是常数!每次调用 g (i) 时它都会发生变化,因此循环仅从 0 到 49 运行!而且由于编译器通常不知道你是否编写了这样的糟糕代码,因此它必须假设 x 在调用 g 时可能会发生变化。因此,您可以预期代码会比使用“int x”慢。

对于许多可能通过引用传递的对象来说,显然也是如此。例如,如果通过 const& 传递对象,并且该对象具有 int 或 unsigned int 的成员,则使用 char*、int* 或 unsigned int* 的任何赋值都可能更改该成员,除非编译器可以证明并非如此。按值传递,证明对编译器来说要容易得多。

评论

0赞 supercat 10/16/2014
如果我正在设计一种语言/框架,我将定义一个参数/参数语义,以应对调用方和被调用方都不允许更改传递的项的情况;在这种情况下,按值传递和按引用传递是等效的,因此编译器可以自动选择更有效的哪个。或者,函数可以定义多个入口点,具体取决于是否可能在调用端修改传入的对象,或者调用方在调用后是否需要该对象,并让被调用的方法在需要时执行复制。
0赞 Demi 10/16/2014
D 就是这样(不可变数据)。许多其他语言也是如此。
0赞 rr- 10/16/2014
它只发生在像这样的“糟糕的代码”中(例如使用全局变量),还是编译器理解变量范围并在正常情况下对 ness 进行有根据的猜测?const
3赞 Андрей Вахрушев 10/16/2014 #5

对于抽象“C++”中的抽象“T”,经验法则是使用更好地反映意图的方式,对于未修改的参数,几乎总是“按值传递”。此外,具体的现实世界编译器期望有这样的抽象描述,并且会以最有效的方式传递你的 T,无论你在源代码中如何做到这一点。

或者,要谈论 naivie 编译和组合,“复制成本非常低”是“您可以在单个寄存器中加载的任何内容”。真的没有比这更便宜的了。

2赞 Alex Shroyer 10/16/2014 #6

如果您要对 by-value 与 by-const-reference 使用“经验法则”,请执行以下操作:

  • 选择一种方法并在任何地方使用它
  • 就所有同事中的哪一个达成一致
  • 只是后来,在“手动调整性能”阶段,才开始改变事物
  • 然后,只有在看到可衡量的改进时才更改它们

评论

6赞 Alex Shroyer 10/16/2014
经验法则并不能取代常识。如果很明显,一种方法在某些情况下更好,那么就没有必要将决策推迟到测试之后;无需测试。使用经验法则来做小决策,其中“浪费时间”因素是以开发时间而不是 CPU 时间来衡量的。
3赞 Eloff 5/17/2016 #7

这里的人是正确的,大多数时候,当结构/类的大小很小并且没有花哨的复制构造函数时,这并不重要。然而,这并不是无知的借口。以下是一些现代 ABI(如 x64)中发生的情况。您可以看到,在该平台上,经验法则的一个很好的阈值是按值传递,其中类型是 POD 类型且 sizeof() <= 16,因为它将在两个寄存器中传递。经验法则很好,它们可以防止您在像这样不会动针的小决定上浪费时间和有限的脑力。

但是,有时这很重要。当你用分析器确定你有这些情况之一时(除非它非常明显,否则不会在之前!),那么你需要了解低级细节 - 而不是听到关于它如何无关紧要的令人欣慰的陈词滥调,这是 SO 的全部。需要注意的一些事项:

  • 按值传递告诉编译器对象不会更改。有一些邪恶的代码,涉及线程或全局变量,其中 const 引用所指向的东西在其他地方被更改。虽然你肯定不会写出邪恶的代码,但编译器可能很难证明这一点,因此必须生成非常保守的代码。
  • 如果寄存器传递的参数太多,那么无论大小是多少,对象都会在堆栈上传递。
  • 今天很小的 POD 对象明天可能会增长并获得昂贵的复制构造函数。添加这些东西的人可能没有检查它是通过引用还是按值传递的,所以曾经表现良好的代码可能会突然崩溃。如果您在没有全面性能回归测试的团队中工作,则通过常量引用是一个更安全的选择。你迟早会被这个咬伤。