C++ 以“const&;的形式传递”轻量级对象
给出以下pet片段: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
模板
结构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