rvalue引用和lvalue引用的开销是否相同?

6
考虑以下例子:
#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)); // ???
}
< p > foo<int&&>(i) 是否像 foo<int>(i) 一样快,还是像 foo<int&>(i) 一样涉及指针开销?

编辑:如建议的那样,运行 g++ -S 给出了相同的 51 行汇编文件用于 foo<int>(i)foo<int&>(i),但 foo<int&&>(std::move(i)) 导致了 71 行汇编代码(看起来差异来自 std::move)。

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


1
过早优化? - Rakete1111
1
从语义上讲,在rvalue引用的情况下进行双重复制是可能的,但在实际情况中,我希望编译器使用指针 - 毕竟,具有真实(而不是由std::move创建的)rvalues(和大型rvalues - 例如,即时构造的std::vector)的代码最好通过非const指针传递。 - Severin Pappadeux
2
运行 g++ -S 命令,我得到了相同的 51 行汇编代码 - 尝试不同的优化级别(-O1 vs --O2 vs -O3 vs -Os)。 - Jesper Juhl
1
我同意 @JesperJuhl 的观点: -S 在没有 -Os 的情况下几乎从不有实际意义,特别是在 -O0 或者 -O3 的情况下更不会有。只有 -S -Os 才能产生近似可读的汇编代码,并且显示出实际发生了什么。即便如此,您的模板 foo<> 实际上甚至没有尝试使用其参数。优化器将会忽略您尝试查看的内容。为了进行适当的分析,请在单独的文件中定义三个非模板函数,例如 int foo_noref(int arg) { return arg; } 并使用 -S -Os 进行编译。然后对调用 void bar_noref() { int i = 0; foo_noref(i); } 进行相同操作。 - cmaster - reinstate monica
1
是的,它添加了指针,因此所有使用所引用对象的地方都将涉及指针间接寻址。另一方面,如果您引用的对象更大,则按值传递可能会调用制作副本的所有成本,即使您随后仅在函数中访问1个成员。通过值将对象传递给另一个方法,您将创建另一个副本。有时这是您想要做的事情,但通常最好按值传递简单值,并按const ref传递较大的对象。 - Gem Taylor
显示剩余6条评论
2个回答

6
在您的特定情况下,它们很可能是相同的。使用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
< p > 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测试)。


我喜欢 volatile int 的技巧。从理论上讲,编译器是否也可以优化掉对 foo(i) 的调用,因为它知道输入被丢弃了? - Taylor Nichols
@TaylorNichols 如果没有volatile,整个程序都会被优化成无效代码:https://godbolt.org/g/e3n6BA。有了volatile,编译器就不知道每次赋值之间是否发生了某些事情(代码中不存在的事情),因此它必须实际执行“正确的操作”,这意味着设置一个值,就好像它调用了该函数一样。 - xaxxon
这很有道理,所以 foo 甚至没有被调用,我们只在每种情况下得到 j = i;,因此出现了 mov 语句。我想我的问题如果 foo 被实际调用会更相关。 - Taylor Nichols
1
好的,那么看起来 ref vs no-ref 有点不同:https://godbolt.org/g/pbZ1BT - xaxxon

5

参数传递效率取决于ABI。

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

3.1.2 引用参数

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

这与引用类别(rvalue/lvalue引用)无关。

为了更全面地了解情况,我在丹麦技术大学的一个文件调用约定中找到了以下引用的描述,该文件分析了大多数编译器:

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

因此,不论ABI如何,rvalue和lvalue引用都需要额外的指针开销。


谢谢 - 我希望能找到一些官方文档。我想rvalue引用可能需要使用指针,因为从对象移动通常涉及从不同的作用域重置其变量。 - Taylor Nichols

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接