【问题标题】:Do rvalue references have the same overhead as lvalue references?右值引用与左值引用具有相同的开销吗?
【发布时间】:2019-01-20 19:41:50
【问题描述】:

考虑这个例子:

#include <utility>

// runtime dominated by argument passing
template <class T>
void foo(T t) {}

int main() {
    int i(0);
    foo<int>(i); // fast -- int is scalar type
    foo<int&>(i); // slow -- lvalue reference overhead
    foo<int&&>(std::move(i)); // ???
}

foo&lt;int&amp;&amp;&gt;(i) 是否与foo&lt;int&gt;(i) 一样快,还是像foo&lt;int&amp;&gt;(i) 那样涉及指针开销?

编辑:按照建议,运行 g++ -S 给了我相同的 51 行汇编文件 foo&lt;int&gt;(i)foo&lt;int&amp;&gt;(i),但 foo&lt;int&amp;&amp;&gt;(std::move(i)) 产生了 71 行汇编代码(看起来差异来自 @ 987654330@)。

编辑:感谢那些推荐具有不同优化级别的 g++ -S 的人——使用 -O3(并制作 foo noinline)我能够获得类似于 xaxxon's solution 的输出。

【问题讨论】:

  • 过早优化?
  • 好吧,从语义上讲,在右值引用的情况下可以进行双重复制,但对于实际情况,我希望编译器使用指针 - 毕竟,带有真实的代码(不是由 std::move 制作的) 右值(和大的右值 - 比如说,即时构造的 std::vector )会更好地使用 pass-by-non-const-pointer
  • “运行 g++ -S 给了我相同的 51 行汇编” - 尝试使用不同的优化级别(-O1 vs --O2 vs -O3 vs -Os)。
  • 我第二个@JesperJuhl:没有-Os-S 几乎没有意义,尤其是没有-O0-O3。只有-S -Os 产生近乎可读的汇编代码,显示实际发生的情况。也就是说,您的模板foo&lt;&gt; 甚至没有尝试 来实际使用它的参数。优化器将丢弃您尝试查看的内容。为了正确分析,定义三个非模板函数,如int foo_noref(int arg) { return arg; }在单独的文件中,并使用-S -Os 编译。然后对调用 void bar_noref() { int i = 0; foo_noref(i); } 执行相同操作。
  • 是的,它添加了指针,因此引用对象的所有使用都将涉及指针间接。另一方面,如果您引用的对象更大,那么按值传递可能会调用制作副本的所有成本,即使您当时只访问了函数中的一个成员。按值将对象传递给另一个方法,然后制作另一个副本。有时这是您想要做的,但通常按值传递简单值并通过 const ref 传递较大对象是一个好策略。

标签: c++ c++11 overhead pass-by-rvalue-reference


【解决方案1】:

在您的具体情况下,它们可能都相同。使用 gcc -O3 从 godbolt 得到的代码是 https://godbolt.org/g/XQJ3Z4

#include <utility>

// runtime dominated by argument passing
template <class T>
int foo(T t) { return t;}

int main() {
    int i{0};
    volatile int j;
    j = foo<int>(i); // fast -- int is scalar type
    j = foo<int&>(i); // slow -- lvalue reference overhead
    j = foo<int&&>(std::move(i)); // ???
}

是:

    mov     dword ptr [rsp - 4], 0 // foo<int>(i);
    mov     dword ptr [rsp - 4], 0 // foo<int&>(i);
    mov     dword ptr [rsp - 4], 0 // foo<int&&>(std::move(i)); 
    xor     eax, eax
    ret

volatile int j 是为了让编译器无法优化掉所有代码,因为否则它会知道调用的结果将被丢弃,整个程序将优化为空。

但是,如果您强制该函数不被内联,那么情况会有所改变int __attribute__ ((noinline)) foo(T t) { return t;}

int foo<int>(int):                           # @int foo<int>(int)
        mov     eax, edi
        ret
int foo<int&>(int&):                          # @int foo<int&>(int&)
        mov     eax, dword ptr [rdi]
        ret
int foo<int&&>(int&&):                          # @int foo<int&&>(int&&)
        mov     eax, dword ptr [rdi]
        ret

以上:https://godbolt.org/g/pbZ1BT

对于此类问题,请学会爱上https://godbolt.orghttps://quick-bench.com/(快速板凳要求您学习如何正确使用google test

【讨论】:

  • 我喜欢volatile int 的把戏。从理论上讲,编译器是否也可以优化对 foo(i) 的调用,因为它知道输入被丢弃?
  • @TaylorNichols 没有 volatile,整个程序优化为空:godbolt.org/g/e3n6BA。对于 volatile,这意味着编译器不知道每次赋值之间不会发生某些事情(代码中不存在的事情),因此它必须实际做“正确的事情”,这意味着设置一个值“好像”它调用了这个函数。
  • 这是有道理的,所以foo 甚至不会被调用,而我们只是在每种情况下都得到j = i;,因此mov 语句。我想如果 foo 实际被调用,我的问题会更相关。
  • 好吧,看起来 ref 和 no-ref 有点不同:godbolt.org/g/pbZ1BT
【解决方案2】:

参数传递的效率取决于 ABI。

例如,在 linux 上,Itanium C++ ABI 指定引用作为指向被引用对象的指针传递:

3.1.2 参考参数

通过传递指向实际参数的指针来处理引用参数。

这与引用类别无关(右值/左值引用)。

从更广泛的角度来看,我在丹麦技术大学的文档calling convention 中找到了这句话,该文档分析了大多数编译器:

引用在所有方面都被视为与指针相同。

所以右值和左值引用涉及所有 ABI 的指针开销。

【讨论】:

  • 谢谢——我希望能找到一些官方文档。我想右值引用必须使用指针之类的东西,因为从一个对象移动通常涉及从不同的范围重置它的变量。
猜你喜欢
  • 2013-12-20
  • 1970-01-01
  • 2022-08-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-31
  • 1970-01-01
相关资源
最近更新 更多