【问题标题】:Access through reference overhead vs copy overhead通过引用开销与复制开销进行访问
【发布时间】:2019-07-01 13:39:51
【问题描述】:

假设我想传递一个 POD 对象作为 const 参数。我知道对于像 int 和 double 这样的简单类型,通过值传递比通过 const 引用更好,因为引用开销。但是在多大的情况下值得作为参考传递呢?

struct arg
{
  ...
}

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

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

即,我应该从多大的 struct arg 开始使用后一种方法?

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

【问题讨论】:

  • 对于这样的问题只有一个答案:测量!其他的都是深奥的
  • 一般来说,您应该始终致力于生成简单且可读/可理解/可维护的代码,而不必担心(过早的)优化。然后,如果性能不是“足够好”™(通常足够足够好)或达到要求,那么您测量和分析并进行基准测试以找到要优化的顶级瓶颈(使用大量的文档和 cmets)。
  • 顺便问一下这个问题的动机是什么?只需通过 const ref 并且仅当您需要副本时才进行复制

标签: c++ performance reference


【解决方案1】:

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;
}

生成assembly:

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;
}

生成assembly:

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

真的有那么重要吗?

通常不会。如果您关心某个特定函数的性能,它可能会被频繁调用,因此是small。因此,它很可能是内联

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

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

生成assembly:

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

瞧。现在一切都没有实际意义。编译器将两个函数内联并合并到一条指令中!

【讨论】:

    【解决方案2】:

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

    如果类的大小略高,那么可能很难预测。差异通常是微不足道的。

    如果类的规模很大,那么复制可能会更慢。也就是说,这一点没有实际意义,因为巨大的对象实际上无法自动存储,因为它是有限的。

    如果函数是内联扩展的,那么可能没有任何区别。

    要确定一个程序在特定系统上是否比另一个程序更快,以及差异是否显着,您可以使用分析器。

    【讨论】:

      【解决方案3】:

      除了其他响应之外,还有优化问题。

      由于它是一个引用,编译器无法知道该引用是否指向一个可变的全局变量。当调用任何源对当前 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);
      }
      

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

      这是一个compiler explorer example,它显示了仅当变量是本地变量时才应用优化。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-11-25
        • 2012-07-04
        • 2014-02-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多