当通过值传递比通过常量引用传递快时的经验法则

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

本文关键字:经验法则 常量 值传 引用      更新时间:2023-10-16

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

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

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

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

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

模板传递值或常量引用或。。。?

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

如果您希望T始终是一个数字类型或一个非常复制起来很便宜,那么你就可以按价值来接受这个论点。

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

如果您有理由怀疑有值得的性能提升,请根据经验法则将其剔除,并进行衡量。您引用的建议的目的是,您不会无故复制大量数据,但也不会因为引用所有内容而危及优化。如果某个东西处于"复制起来明显便宜"answers"复制起来显然昂贵"之间的边缘,那么你可以负担得起这两种选择。如果你必须放弃这个决定,那就掷硬币吧。

如果一个类型没有时髦的复制构造函数,并且它的sizeof很小,那么它的复制成本很低。"small"没有一个硬数字是最优的,甚至在每个平台上都没有,因为它在很大程度上取决于调用代码和函数本身。凭直觉行事。一、二、三个单词很小。十,谁知道呢。4x4矩阵并不小。

传递一个值而不是常量引用的优点是编译器知道值不会改变。"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!由于编译器通常不知道你是否写了这样糟糕的代码,所以它必须假设当g被调用时,x可能会发生变化。因此,您可以预期代码会比使用"int x"时慢。

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

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

sizeof(T) >= sizeof(T*)

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

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

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

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

在我的代码语义正确后,我会对它进行评测,看看我是否在进行任何不必要的复制;我将相应地修改这些内容。

我相信这种方法将帮助我专注于软件中最重要的部分:正确性。我不会妨碍编译器的工作——干涉;抑制---执行优化(我知道我无法战胜它)。

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

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

或者,谈论naivie编译和合成,"复制起来非常便宜"是"任何可以在单个寄存器中加载的东西"。没有比这更便宜的了。

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

然而,有时这很重要。当你用剖析器确定你有其中一个案例时(除非它非常明显,否则之前不会!),那么你需要了解低级别的细节——而不是听到令人欣慰的陈词滥调,说这无关紧要,SO充满了这些。需要记住的一些事项:

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

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

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