要回答这个问题,您必须查看底层平台。适用于一个的技术和实践可能不适用于另一个。因此,为了便于讨论,让我们关注 x86_64 和传递整数类型参数。其他架构和调用约定可以类似。
规则总结如下:
-
对于整型对象和浮点数/双精度值传递(解释原因如下)
-
对于像 std::optional 这样较小的对象和仅包含类型 1 值的小型结构,您仍然可以按值传递。
-
对于任何较大的对象,通过 const 引用或引用传递。
-
特别是对于 std::string,在你的函数中使用 std::string_view 因为它允许你传递一个 char 指针或 char 数组并且不会创建 std::string 临时。
-
现代 C++ 引入了“移动语义”和 && 运算符。这会创建其他类的对象,允许您“接管”传递参数的内容,而不是制作副本。这种技术对于大型物体非常有用。
下面有更详细的解释。仅使用整数(包括指针)调用方法时,使用以下寄存器序列:%rdi、%rsi、%rdx、%rcx、%r8 和 %r9。
对于返回值,使用 %rax 和 %rdx。所有这些寄存器都是 64 位的。
这与 i386 调用语义背道而驰,在 i386 调用语义中,所有内容都在堆栈上传递,即在调用之前必须将每个参数存储在内存中。随着 AMD64 ABI 实现寄存器传递,它变得更快了,因为所有操作都在 CPU 内核内部进行,无需访问内存。
这样的函数
int func( int a, int b );
将使用%rdi=a %rsi=b 并且返回值将在%rax 中。注意,如果func是一个类的方法,第一个参数是this指针,所以序列是%rdi=this%rsi=a%rdx=b,返回值是%rax。
如果你通过引用传递那个 int 会发生什么?比较一下吧。
int func( int a, int b ) {
return a+b;
}
int func( int& a, int& b ) {
return a+b;
}
编译时会产生
func(int, int): # @func(int, int)
leal (%rdi,%rsi), %eax
retq
func(int const&, int const&): # @func(int const&, int const&)
movl (%rsi), %eax
addl (%rdi), %eax
retq
所以请注意,引用是作为 POINTER 传递的,这将导致两个更昂贵的操作,即内存提取 movl (%rsi), %eax 加上 add 本身,而不是一个简单的求和 leal (%rdi,%rsi), %eax,无需访问内存即可完成。
因此,在这种情况下,在处理整数(类 int)值时,通过值而不是引用传递速度和缓存使用率要好得多。
上述同样适用于浮点数和双精度数。寄存器不同(使用 %xmm0 等),但适用相同的逻辑。
对于较大的对象,如 std::vector 或 std::string,如果您不打算在该函数或方法的主体中修改此对象,建议通过 const 引用传递。如果您需要修改它们,请通过引用传递。这样做,整数类型的相同规则将适用,因为指针和引用被认为是类似整数的。
例如
#include <string>
int len( const std::string& s ) {
return s.size();
}
int call( const std::string& s ) {
return len(s);
}
将屈服于
len(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&): # @len(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
movl 8(%rdi), %eax
retq
call(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&): # @call(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
movl 8(%rdi), %eax
retq
但是如果你像这样通过值传递一个字符串
int len2( std::string s ) {
return s.size();
}
int call2( std::string s ) {
return len2(s);
}
那么len2 方法本身仍然很简单,但调用者必须复制它并导致调用者非常大
len2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @len2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
movl 8(%rdi), %eax
retq
call2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >): # @call2(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
pushq %r15
pushq %r14
pushq %r12
pushq %rbx
subq $40, %rsp
leaq 24(%rsp), %r12
movq %r12, 8(%rsp)
movq (%rdi), %r14
movq 8(%rdi), %rbx
cmpq $15, %rbx
jbe .LBB7_1
testq %rbx, %rbx
js .LBB7_12
movq %rbx, %rdi
incq %rdi
js .LBB7_13
callq operator new(unsigned long)
movq %rax, %r15
movq %rax, 8(%rsp)
movq %rbx, 24(%rsp)
testq %rbx, %rbx
jne .LBB7_6
jmp .LBB7_9
.LBB7_1: # %entry.if.end_crit_edge.i.i
movq %r12, %r15
testq %rbx, %rbx
je .LBB7_9
.LBB7_6: # %if.end.i.i
cmpq $1, %rbx
jne .LBB7_8
movb (%r14), %al
movb %al, (%r15)
jmp .LBB7_9
.LBB7_8: # %if.end.i.i.i.i.i
movq %r15, %rdi
movq %r14, %rsi
movq %rbx, %rdx
callq memcpy@PLT
.LBB7_9: # %_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC2ERKS4_.exit
movq %rbx, 16(%rsp)
movb $0, (%r15,%rbx)
movq 8(%rsp), %rdi
movq 16(%rsp), %rbx
cmpq %r12, %rdi
je .LBB7_11
callq operator delete(void*)
.LBB7_11: # %_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED2Ev.exit
movl %ebx, %eax
addq $40, %rsp
popq %rbx
popq %r12
popq %r14
popq %r15
retq
.LBB7_13: # %if.end.i.i.i.i.i.i
callq std::__throw_bad_alloc()
.LBB7_12: # %if.then.i.i.i
movl $.L.str, %edi
callq std::__throw_length_error(char const*)
.L.str:
.asciz "basic_string::_M_create"
参考:https://uclibc.org/docs/psABI-x86_64.pdf