提问人:gexicide 提问时间:10/16/2014 最后编辑:Communitygexicide 更新时间:5/17/2016 访问量:6559
按值传递比按常量引用传递更快的经验法则?
Rule of thumb for when passing by value is faster than passing by const reference?
问:
假设我有一个函数,它接受一个类型的参数。
它不会改变它,所以我可以选择通过常量引用或值传递它: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 始终是数值类型或非常 复制成本低廉,然后您可以按值获取参数。
因此,它并没有解决我的问题,而是重新表述了它:一个类型必须有多小才能“复制成本非常低”?
答:
在我看来,最合适的经验法则是在以下情况下通过引用:
sizeof(T) >= sizeof(T*)
这背后的想法是,当你通过引用时,最坏的情况是你的编译器可能会使用指针来实现这一点。
当然,这没有考虑到复制构造函数和移动语义的复杂性,以及围绕对象生命周期可以创建的所有地狱。
此外,如果你不关心微优化,你可以通过常量引用传递所有内容,在大多数机器上,指针是 4 或 8 个字节,很少有类型比这更小,即使在这种情况下,你也会丢失一些(小于 8)字节的复制操作和一些间接操作,这在现代世界中很可能不会成为你的瓶颈:)
评论
如果您有理由怀疑有值得的性能提升,请根据经验和衡量法则将其剔除。您引用的建议的目的是,您不会无缘无故地复制大量数据,但也不要通过将所有内容作为参考来危及优化。如果某些东西介于“复制成本明显低”和“复制成本明显昂贵”之间,那么您可以负担得起任何一种选择。如果你必须把决定权从你身上拿走,那就掷硬币。
如果一个类型没有时髦的复制构造函数并且它很小,那么复制它的成本很低。对于“小”来说,没有最优的硬性数字,即使是在每个平台上也是如此,因为它在很大程度上取决于调用代码和函数本身。只要凭直觉就行了。一、二、三个字都很小。十个,谁知道呢。一个 4x4 矩阵并不小。sizeof
评论
struct A { std::list<double> x; };
sizeof
我相信我会尽可能地选择按值传递(即:当语义要求我不需要实际对象来工作时)。我相信编译器会执行适当的移动和复制省略。
在我的代码在语义上正确之后,我会对其进行分析,看看我是否正在制作任何不必要的副本;我会相应地修改这些内容。
我相信这种方法将帮助我专注于软件中最重要的部分:正确性。而且我不会妨碍编译器---干扰;inhibit---执行优化(我知道我无法击败它)。
话虽如此,名义上的引用是作为指针实现的。因此,在真空中,在不考虑语义、复制省略、移动语义和类似的东西的情况下,通过指针/引用传递任何大小大于指针的东西会更“有效”。
评论
传递值而不是 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* 的任何赋值都可能更改该成员,除非编译器可以证明并非如此。按值传递,证明对编译器来说要容易得多。
评论
const
对于抽象“C++”中的抽象“T”,经验法则是使用更好地反映意图的方式,对于未修改的参数,几乎总是“按值传递”。此外,具体的现实世界编译器期望有这样的抽象描述,并且会以最有效的方式传递你的 T,无论你在源代码中如何做到这一点。
或者,要谈论 naivie 编译和组合,“复制成本非常低”是“您可以在单个寄存器中加载的任何内容”。真的没有比这更便宜的了。
如果您要对 by-value 与 by-const-reference 使用“经验法则”,请执行以下操作:
- 选择一种方法并在任何地方使用它
- 就所有同事中的哪一个达成一致
- 只是后来,在“手动调整性能”阶段,才开始改变事物
- 然后,只有在看到可衡量的改进时才更改它们
评论
这里的人是正确的,大多数时候,当结构/类的大小很小并且没有花哨的复制构造函数时,这并不重要。然而,这并不是无知的借口。以下是一些现代 ABI(如 x64)中发生的情况。您可以看到,在该平台上,经验法则的一个很好的阈值是按值传递,其中类型是 POD 类型且 sizeof() <= 16,因为它将在两个寄存器中传递。经验法则很好,它们可以防止您在像这样不会动针的小决定上浪费时间和有限的脑力。
但是,有时这很重要。当你用分析器确定你有这些情况之一时(除非它非常明显,否则不会在之前!),那么你需要了解低级细节 - 而不是听到关于它如何无关紧要的令人欣慰的陈词滥调,这是 SO 的全部。需要注意的一些事项:
- 按值传递告诉编译器对象不会更改。有一些邪恶的代码,涉及线程或全局变量,其中 const 引用所指向的东西在其他地方被更改。虽然你肯定不会写出邪恶的代码,但编译器可能很难证明这一点,因此必须生成非常保守的代码。
- 如果寄存器传递的参数太多,那么无论大小是多少,对象都会在堆栈上传递。
- 今天很小的 POD 对象明天可能会增长并获得昂贵的复制构造函数。添加这些东西的人可能没有检查它是通过引用还是按值传递的,所以曾经表现良好的代码可能会突然崩溃。如果您在没有全面性能回归测试的团队中工作,则通过常量引用是一个更安全的选择。你迟早会被这个咬伤。
评论
sizeof(std::vector<int>)
在某些 32 位实现中为 12,在某些 64 位实现中为 24,但您不想按值传递它,因为内容可能远不止于此,即使情况并非如此,复制构造也需要为任何非空向量分配内存......另一方面,这很大程度上取决于你所针对的平台,你应该查看 ABI 来了解函数调用是如何实现的。std::vector
vector