繁体   English   中英

通过参考开销与副本开销进行访问

[英]Access through reference overhead vs copy overhead

假设我要传递POD对象以用作const参数。 我知道对于int和double值这样的简单类型,由于存在引用开销,因此比const引用要好。 但是以什么大小作为参考值得?

struct arg
{
  ...
}

void foo(const arg input)
{
  // read from input
}

要么

void foo(const arg& input)
{
  // read from input
}

即,我应该以哪种大小的arg结构开始使用后一种方法?

我还应该提到,我这里不是在谈论复制省略。 让我们假设它不会发生。

TL; DR:这在很大程度上取决于目标体系结构,编译器和调用函数的上下文。 如果不确定,请分析并手动检查生成的代码。

如果内联函数,则在两种情况下,良好的优化编译器都可能会发出完全相同的代码。

但是,如果内联函数,则大多数C ++实现中的ABI都要求传递const&参数作为指针。 这意味着该结构必须存储在RAM中,这样才能获得其地址。 这可能会对小对象的性能产生重大影响。

让我们以x86_64 Linux G ++ 8.2为例...

具有2个成员的结构:

struct arg
{
    int a;
    long b;
};

int foo1(const arg input)
{
    return input.a + input.b;
}

int foo2(const arg& input)
{
    return input.a + input.b;
}

生成的程序集

foo1(arg):
        lea     eax, [rdi+rsi]
        ret
foo2(arg const&):
        mov     eax, DWORD PTR [rdi]
        add     eax, DWORD PTR [rdi+8]
        ret

第一个版本完全通过寄存器传递结构,第二个版本通过堆栈传递。

现在让我们尝试3个成员

struct arg
{
    int a;
    long b;
    int c;
};

int foo1(const arg input)
{
    return input.a + input.b + input.c;
}

int foo2(const arg& input)
{
    return input.a + input.b + input.c;
}

生成的程序集

foo1(arg):
        mov     eax, DWORD PTR [rsp+8]
        add     eax, DWORD PTR [rsp+16]
        add     eax, DWORD PTR [rsp+24]
        ret
foo2(arg const&):
        mov     eax, DWORD PTR [rdi]
        add     eax, DWORD PTR [rdi+8]
        add     eax, DWORD PTR [rdi+16]
        ret

尽管使用第二个版本仍然会比较慢,因为使用第二个版本会需要将地址放入rdi因此不再那么多。

真的有那么重要吗?

通常不会。 如果您关心某个特定功能的性能,那么它可能会被频繁调用,因此它很小 这样,很可能会内联

让我们尝试调用上面的两个函数:

int test(int x)
{
    arg a {x, x};
    return foo1(a) + foo2(a);
}

生成的程序集

test(int):
        lea     eax, [0+rdi*4]
        ret

瞧。 现在都没事了。 编译器将两个函数内联并合并为一条指令!

合理的经验法则:如果类的大小等于或小于指针的大小,则复制可能会快一些。

如果班级人数稍大,则可能很难预测。 差异通常很小。

如果班级人数众多,则复制可能会变慢。 就是说,要点是没有意义的,因为在实践中大型对象无法自动存储,因为它是受限制的。

如果将函数内联展开,则可能没有任何区别。

要查明某个程序在特定系统上是否比另一个程序快,以及两者之间的区别是否显着,可以使用探查器。

除了其他响应之外,还存在优化方面的问题。

由于它是引用,因此编译器无法知道引用是否指向可变的全局变量。 当调用任何源无法用于当前TU的函数时,编译器必须假定变量可能已被突变。

例如,如果您有一个if依赖于Foo的数据成员,则调用一个函数,然后使用相同的数据成员,则将强制编译器输出两个备用负载,而如果该变量是局部变量,则它知道不能在其他地方变异。 这是一个例子:

struct Foo {
    int data;
};

extern void use_data(int);

void bar(Foo const& foo) {
    int const& data = foo.data;

    // may mutate foo.data through a global Foo
    use_data(data);

    // must load foo.data again through the reference
    use_data(data);
}

如果变量是局部变量,则编译器将简单地重用寄存器中已经存在的值。

这是一个编译器浏览器示例 ,该示例显示仅在变量为局部变量时才应用优化。

这就是为什么“一般建议”将为您提供良好的性能,但不会为您提供最佳性能的原因。 如果您真正关心代码的性能,则必须确定并分析代码。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM