【问题标题】:Rule of thumb for when passing by value is faster than passing by const reference?按值传递比通过 const 引用传递更快的经验法则?
【发布时间】:2014-12-10 19:50:10
【问题描述】:

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

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

在通过 const 引用传递比通过值传递更便宜之前,T 应该变成多大的经验法则?例如,假设我知道sizeof(T) == 24。我应该使用 const 引用还是值?

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

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

template pass by value or const reference or...?

但是,接受的答案( https://stackoverflow.com/a/4876937/1408611 ) 没有说明任何细节,它只是说明:

如果您希望 T 始终是数字类型或非常 复制成本低,那么您可以按值获取参数。

所以它并没有解决我的问题,而是改写了它:一个类型必须多小才能“复制起来非常便宜”?

【问题讨论】:

  • @TheOne 一点也不。您必须考虑通过您获得的指针加载您使用的参数部分的成本。这很快就会变得混乱,所以最好的方法是尝试一下。但是例如,如果您的唯一参数是两个整数的 POD 对,则按值传递可能(取决于 ABI)只使用两个寄存器,而按指针传递会浪费可用的寄存器,并且需要函数内部的额外指令。
  • sizeof(std::vector<int>) 在某些 32 位实现中为 12,在某些 64 位实现中为 24,但您不想按值传递它,因为 contents 可能更多不仅如此,即使不是这种情况,复制构造也需要为任何非空向量分配内存...另一方面,这在很大程度上取决于您所针对的平台,您应该查看 ABI 以了解函数调用是如何实现的。
  • 相关阅读:Want speed? Pass by value. 当然还有谷歌搜索标题以阅读该文章的所有支持点和对立点。
  • @Angew 一篇不错的文章,但它专门讨论了您需要副本的情况(因为您要对其进行变异)。
  • @DavidRodríguez-dribeas:我指定我假设一个微不足道的复制构造函数。 std::vector 当然是一个非常普通的复制构造函数的例子,所以我什至不会考虑复制vector,除非我真的需要一个副本。

标签: c++ c++11 pass-by-reference pass-by-value


【解决方案1】:

如果您有理由怀疑会有值得的性能提升,请使用经验法则将其剔除并衡量。您引用的建议的目的是不要无缘无故地复制大量数据,但也不要通过将所有内容都作为参考来危害优化。如果某些东西处于“复制明显便宜”和“复制明显昂贵”之间,那么您可以负担得起任何一种选择。如果您必须让您做出决定,请掷硬币。

如果一个类型没有时髦的复制构造函数并且它的sizeof 很小,那么它的复制成本很低。 “小”没有最佳的硬数字,甚至在每个平台的基础上都没有,因为它在很大程度上取决于调用代码和函数本身。随你的直觉走吧。一、二、三个字很小。十个,谁知道呢。一个 4x4 的矩阵并不小。

【讨论】:

  • 还取决于调用此函数的频率。衡量的另一个原因。
  • 如果一个类型没有时髦的复制构造函数并且它的 sizeof 很小,那么它的复制成本很低。 - 这可能会产生误导。这里需要注意的是,一个类可能有一个“时髦的复制构造函数”,而程序员并不知道它,例如struct A { std::list<double> x; };。这样的类可以有一个小的 sizeof 并且没有显式的复制构造函数,但复制起来仍然可能非常昂贵。
【解决方案2】:

我认为最合适的经验法则是在以下情况下通过引用传递:

sizeof(T) >= sizeof(T*)

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

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

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

【讨论】:

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

传递一个值而不是一个 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* 的任何赋值可能更改该成员, 除非编译器可以证明。通过值传递,编译器的证明要容易得多。

【讨论】:

  • 如果我正在设计一种语言/框架,我会为调用者和被调用方法都不允许更改传递项的情况定义参数/参数语义;在这种情况下,按值传递和按引用传递将是等效的,因此编译器可以自动选择更有效的那个。或者,一个函数可以根据传入的对象是否可能在调用端被修改或调用者在调用后是否需要该对象来定义多个入口点,并让被调用的方法在需要时执行复制。
  • D 就是这样(不可变数据)。许多其他语言也是如此。
  • 只有像这样的“糟糕的代码”(例如使用全局变量)才会发生这种情况,还是编译器理解变量范围并在正常情况下对constness 做出有根据的猜测?
【解决方案4】:

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

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

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

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

【讨论】:

  • 正如我之前在多个 cmets 中解释的那样,不,大于指针并不意味着通过引用传递更有效,即使在你的真空中也不行。
【解决方案5】:

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

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

【讨论】:

    【解决方案6】:

    如果您要对按值与按常量引用使用“经验法则”,请执行以下操作:

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

    【讨论】:

    • 经验法则不能取代常识。如果在某些情况下明显地认为一种方法更好,则无需将决策推迟到测试之后;并且无需测试。对于“浪费时间”因素以开发时间而非 CPU 时间衡量的次要决策,请使用经验法则。
    【解决方案7】:

    这里的人是正确的,大多数时候结构/类的大小很小并且没有花哨的复制构造函数并不重要。然而,这并不是无知的借口。这是 x64 等现代 ABI 中的what happens。您可以看到,在该平台上,您的经验法则的一个很好的阈值是在类型为 POD 类型且 sizeof()

    但是,有时这很重要。当您使用分析器确定您有其中一种情况时(除非它非常明显!),那么您需要了解底层细节 - 不要听到关于它如何无关紧要的令人欣慰的陈词滥调,所以充满了。需要记住的一些事项:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-20
      • 1970-01-01
      • 1970-01-01
      • 2015-03-24
      • 2014-12-20
      • 2023-03-03
      • 2012-06-14
      • 2019-08-22
      相关资源
      最近更新 更多