C++ 以“const&;的形式传递”轻量级对象

C++ 以“const&;的形式传递”轻量级对象,c++,pass-by-reference,pass-by-value,micro-optimization,C++,Pass By Reference,Pass By Value,Micro Optimization,给出以下pet片段: 模板 结构my_对{/*构造函数和类似的*/}; 自动f(标准::成对常数和p)/(1) {返回我的_对(p.first,p.second);} 自动f(标准::对p)/(2) {返回我的_对(p.first,p.second);} 如果我知道T1和T2都是轻量级对象,它们的复制时间可以忽略不计(例如,每个都有两个指针),那么将std::pair作为副本传递比作为引用传递好吗?因为我知道有时候让编译器省略副本比强迫它处理引用(例如,优化副本链)要好 同样的问题也适用于my

给出以下pet片段:

模板
结构my_对{/*构造函数和类似的*/};
自动f(标准::成对常数和p)/(1)
{返回我的_对(p.first,p.second);}
自动f(标准::对p)/(2)
{返回我的_对(p.first,p.second);}
如果我知道
T1
T2
都是轻量级对象,它们的复制时间可以忽略不计(例如,每个都有两个指针),那么将
std::pair
作为副本传递比作为引用传递好吗?因为我知道有时候让编译器省略副本比强迫它处理引用(例如,优化副本链)要好

同样的问题也适用于
my_pair
的构造函数,如果让它们接收副本比接收引用更好的话

调用上下文未知,但对象生成器和类构造函数本身都是内联函数,因此引用和值之间的差异可能并不重要,因为优化器可以看到最终目标并在最后应用构造(我只是推测),因此,对象生成器将是纯零开销抽象,在这种情况下,我认为引用会更好,以防某些异常对比通常的更大


但如果不是这样(引用总是或通常对副本有一些影响,即使所有内容都是内联的),那么我会选择副本。

在微优化领域之外,我通常会传递一个
常量
引用,因为您不修改对象,并且希望避免副本。如果有一天您确实使用了成本高昂的
T1
T2
,那么复制可能是一个大问题:传递常量引用时没有等效的大脚枪。因此,我将按值传递视为一种具有非常不对称权衡的选择,并且仅当我知道数据很小时才按值进行选择

至于具体的微优化问题,它基本上取决于调用是否完全内联以及编译器是否合适

全内联 如果
f
函数的任一变体内联到调用者中,并且启用了优化,则很可能会得到与任一变体相同或几乎相同的代码。我用
inline_f_ref
inline_r_val
调用来测试这一点。它们都从未知的外部函数生成
,然后通过引用或通过
f
的变量调用

类似于
f_val
f_ref
版本仅在末尾更改调用):

完全一样。编译器正确地看穿了函数,甚至识别出
std::pair
mypair
实际上具有相同的布局,因此
f
的所有跟踪都会消失

这里的版本中,
T1
T2
是一个具有两个指针的结构,而不是:

auto inline_f_ref<twop>():
        push    r12
        mov     r12, rdi
        sub     rsp, 32
        mov     rdi, rsp
        call    std::pair<twop, twop> get_pair<twop>()
        mov     rax, QWORD PTR [rsp]
        mov     QWORD PTR [r12], rax
        mov     rax, QWORD PTR [rsp+8]
        mov     QWORD PTR [r12+8], rax
        mov     rax, QWORD PTR [rsp+16]
        mov     QWORD PTR [r12+16], rax
        mov     rax, QWORD PTR [rsp+24]
        mov     QWORD PTR [r12+24], rax
        add     rsp, 32
        mov     rax, r12
        pop     r12
        ret
std::pair
的指针被传递到
rdi
,因此函数体是从该位置到
rax
的单个8字节移动。std::pair需要8个字节,因此编译器一次复制整个内容。在这种情况下,返回值在
rax
中“按值”传递,因此我们就完成了

这取决于编译器的优化能力和ABI。例如,以下是MSVC为64位Windows目标编译的相同函数:

my_pair<int,int> f_ref<int,int>(std::pair<int,int> const &) PROC ; f_ref<int,int>, COMDAT
        mov     eax, DWORD PTR [rdx]
        mov     r8d, DWORD PTR [rdx+4]
        mov     DWORD PTR [rcx], eax
        mov     rax, rcx
        mov     DWORD PTR [rcx+4], r8d
        ret     0
仍然只有一条指令,但这一次是一条reg reg move,它永远不会比加载更昂贵,而且通常相当便宜

在MSVC上,64位Windows:

my_pair<int,int> f_val<int,int>(std::pair<int,int>)
        mov     rax, rdx
        mov     DWORD PTR [rcx], edx
        shr     rax, 32                             ; 00000020H
        mov     DWORD PTR [rcx+4], eax
        mov     rax, rcx
        ret     0
以下是按值计算的版本:

auto f_val<twop, twop>(std::pair<twop, twop>):
        mov     rdx, QWORD PTR [rsp+8]
        mov     rax, rdi
        mov     QWORD PTR [rdi], rdx
        mov     rdx, QWORD PTR [rsp+16]
        mov     QWORD PTR [rdi+8], rdx
        mov     rdx, QWORD PTR [rsp+24]
        mov     QWORD PTR [rdi+16], rdx
        mov     rdx, QWORD PTR [rsp+32]
        mov     QWORD PTR [rdi+24], rdx
auto f_val(std::pair):
mov rdx,QWORD PTR[rsp+8]
莫夫拉克斯,rdi
mov QWORD PTR[rdi],rdx
mov rdx,QWORD PTR[rsp+16]
mov QWORD PTR[rdi+8],rdx
mov rdx,QWORD PTR[rsp+24]
mov QWORD PTR[rdi+16],rdx
mov rdx,QWORD PTR[rsp+32]
mov QWORD PTR[rdi+24],rdx
尽管加载和存储的顺序不同,但它们都在做完全相同的事情:4个加载和4个存储,将32字节从输入复制到输出。唯一真正的区别是,在按值情况下,对象应位于堆栈上(因此我们从
[rsp]
复制),而在按引用情况下,对象由第一个参数指向,因此我们从
[rdi
]1复制

因此,有一个较小的窗口,其中非内联的按值函数比按引用传递具有优势:在该窗口中,它们的参数可以在寄存器中传递。对于Sys V ABI,这通常适用于最多16个字节的结构,在Windows x86-64 ABI上适用于最多8个字节的结构。还有其他限制,因此并非所有此大小的对象都始终在寄存器中传递



1您可能会说,
rdi
接受第一个参数,而不是
rsi
——但这里发生的是返回值也必须通过内存传递,因此,隐式使用了一个隐藏的第一个参数—返回值的目标缓冲区指针—并进入
rdi

,这不仅是您需要担心的调用,还有间接性,如果您传递指针,它必须通过地址执行内存查找,如果您传递一个原始值,它通常会传递到寄存器中,因此可以从CPU内部直接访问。C++通常通过将结构分解为它们的单个值并通过调用约定传递这些结构来优化结构的传递。如果该对中的两个值都适合一个寄存器(大多数(如果不是所有)原语都应该能够这样做),那么按值传递可能会更快。优化器在内联过程中可能会忽略引用。这真的取决于你。如果您可以保证类型始终适合,则可以按值传递。正如我
my_pair<int,int> f_ref<int,int>(std::pair<int,int> const &) PROC ; f_ref<int,int>, COMDAT
        mov     eax, DWORD PTR [rdx]
        mov     r8d, DWORD PTR [rdx+4]
        mov     DWORD PTR [rcx], eax
        mov     rax, rcx
        mov     DWORD PTR [rcx+4], r8d
        ret     0
auto f_val<int, int>(std::pair<int, int>):
        mov     rax, rdi
        ret
my_pair<int,int> f_val<int,int>(std::pair<int,int>)
        mov     rax, rdx
        mov     DWORD PTR [rcx], edx
        shr     rax, 32                             ; 00000020H
        mov     DWORD PTR [rcx+4], eax
        mov     rax, rcx
        ret     0
auto f_ref<twop, twop>(std::pair<twop, twop> const&):
        mov     rax, rdi
        mov     r8, QWORD PTR [rsi]
        mov     rdi, QWORD PTR [rsi+8]
        mov     rcx, QWORD PTR [rsi+16]
        mov     rdx, QWORD PTR [rsi+24]
        mov     QWORD PTR [rax], r8
        mov     QWORD PTR [rax+8], rdi
        mov     QWORD PTR [rax+16], rcx
        mov     QWORD PTR [rax+24], rdx
auto f_val<twop, twop>(std::pair<twop, twop>):
        mov     rdx, QWORD PTR [rsp+8]
        mov     rax, rdi
        mov     QWORD PTR [rdi], rdx
        mov     rdx, QWORD PTR [rsp+16]
        mov     QWORD PTR [rdi+8], rdx
        mov     rdx, QWORD PTR [rsp+24]
        mov     QWORD PTR [rdi+16], rdx
        mov     rdx, QWORD PTR [rsp+32]
        mov     QWORD PTR [rdi+24], rdx