让我们看看四种情况。
这些是由gcc编译的64位x86架构,不同编译器应该有类似的结果。
- 函数是如何编译的:
bool by_value(__int128 large_number) {
return large_number > 10000;
}
bool by_reference(const __int128& large_number) {
return large_number > 10000;
}
我们可以在这里看到x86汇编输出 https://godbolt.org/z/v9cM8xj35
by_value(__int128):
mov eax, 10000
cmp rax, rdi # Use first 8 bytes
mov eax, 0
sbb rax, rsi # Use second 8 bytes
setl al
ret
by_reference(__int128 const&):
mov eax, 10000
cmp rax, QWORD PTR [rdi] # Use first 8 bytes
mov eax, 0
sbb rax, QWORD PTR [rdi+8] # Use second 8 bytes
setl al
ret
被注释的代码行是唯一不同的代码行。
这个展示了平台的调用约定:前8个字节的参数存储在rdi
中,第二个8字节存储在rsi
中。
当您通过值传递时,large_number
将被存储在这两个寄存器中,并且可以快速高效地使用。
当您通过引用传递时,只使用一个寄存器来传递指向该值的指针(rdi
),并且使用解引用QWORD PTR [rdi]
来访问前8个字节,使用QWORD PTR [rdi+8]
来访问另外8个字节(一些指针算术)。
在大多数情况下,通过值传递会胜出。如果函数中有很多参数或局部变量,则用于存储large_number
的寄存器可能会“溢出”到堆栈上,因此从理论上讲,通过值传递需要做更多的工作。但是,如果存在一个单寄存器指针或两个寄存器16字节的值,那么它可能会溢出,因此实际上应该没有太大的区别。
- 使用现有的
__int128
变量调用函数:
bool by_value(__int128);
bool by_reference(const __int128&);
extern __int128 x;
extern bool call_by_value() {
return by_value(x);
}
extern bool call_by_reference() {
return by_reference(x);
}
https://godbolt.org/z/7sT8b33Ez
call_by_value():
mov rdi, QWORD PTR x[rip]
mov rsi, QWORD PTR x[rip+8]
jmp by_value(__int128)
call_by_reference():
mov edi, OFFSET FLAT:x
jmp by_reference(__int128 const&)
在按值传递的情况下可能需要更多工作:要调用按引用传递,您只需要将
x
(
OFFSET FLAT:x
)的地址加载到
edi
中并调用函数,而在按值传递的情况下,需要将
x
的值读入两个寄存器中,然后才能调用函数。
但是,请记住,
按引用传递
将必须通过指针间接使用它。因此,按引用传递在函数内隐藏了
x[rip]
和
x[rip+8]
,并没有太大区别。
使用某些常量值(或优化为其)调用函数:
bool call_by_value() {
__int128 abc = 20000;
return by_value(abc);
}
bool call_by_reference() {
__int128 abc = 20000;
return by_reference(abc);
}
https://godbolt.org/z/6jhEWfh6a
call_by_value():
mov edi, 20000 # Stores 2000 into the first register
xor esi, esi # Stores 0 into the second register
jmp by_value(__int128)
call_by_reference():
sub rsp, 24
mov rdi, rsp # Store current stack pointer (which will point to abc)
mov QWORD PTR [rsp], 20000 # Store first 8 bytes on stack
mov QWORD PTR [rsp+8], 0 # Store second 8 bytes on the stack
call by_reference(__int128 const&)
add rsp, 24
ret
按引用调用需要做很多事情:值必须分配到堆栈上,然后将指向它的指针传递给函数。
按值调用只需将值存储在两个寄存器中并调用函数。
- 使用运行时计算的prvalue(这里的“计算”只是一次复制)调用函数
bool call_by_value() {
return by_value(+x);
}
bool call_by_reference() {
return by_reference(+x);
}
https://godbolt.org/z/vqdGEeGY9
call_by_value():
mov rdi, QWORD PTR x[rip]
mov rsi, QWORD PTR x[rip+8]
jmp by_value(__int128)
call_by_reference():
sub rsp, 24
movdqa xmm0, XMMWORD PTR x[rip] # Store the value of x into a 16 byte register
mov rdi, rsp # Store current stack pointer
movaps XMMWORD PTR [rsp], xmm0 # Write 16 bytes to the stack pointer
call by_reference(__int128 const&)
add rsp, 24
ret
因此,在按值传递的情况下,计算结果可以直接在寄存器上进行。而在按引用传递的情况下,需要先计算出值,然后将其存储到堆栈中,最后需要传递指针。
还有一个问题:当您使用 extern bool by_reference(const __int128&);
,且没有启用整个程序优化或链接时优化时,编译器无法知道传递给 by_reference
的值是否会被修改。毕竟,这可能看起来像:
bool by_reference(const __int128& large_number) {
const_cast<__int128&>(large_number) = 0;
}
这可能会禁用一些进一步的优化。
总的来说,在大多数情况下,传递值更好。在其他架构上,默认的调用约定可能是在堆栈上传递16字节参数,这将使两种情况没有太大区别。
有些人会说,你只应该通过值传递指针大小或更小的内容,其他所有内容都应该通过引用传递。然而,这未能考虑到寄存器比堆栈快得多。
这是基于汇编分析的结果,而不是实际计时结果。你可能需要多次调用函数才能看到差异。
__int128
可以通过两个寄存器按值传递,完全避免了内存。因此,这应该比按引用传递更好。当然,如果要调用函数的频率很高,参数传递开销非常大,那么它可能应该被内联。 - Nate Eldredge